one-code-extension 0.4.0 → 0.5.0
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/README.md +219 -219
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +66 -34
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +123 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +52 -0
- package/extensions/background/wakeup.ts +159 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +88 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +72 -19
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +198 -31
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +13 -5
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +42 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/local-command.ts +47 -13
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +308 -64
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +13 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +113 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +149 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +143 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +70 -4
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +87 -5
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +209 -71
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +151 -67
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +61 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +12 -7
- package/extensions/recap/index.ts +65 -60
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +175 -13
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +105 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +19 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a compaction cuts the conversation. Pure: no pi extension API.
|
|
3
|
+
*
|
|
4
|
+
* pi keeps the last ~20k tokens verbatim after its summary (`keepRecentTokens`).
|
|
5
|
+
* Claude Code keeps only the final assistant reply (findings §47), and
|
|
6
|
+
* so does One Code: the summary, that reply, then whatever came after it (the
|
|
7
|
+
* results of a tool call it made, on an overflow mid-turn). A short kept tail
|
|
8
|
+
* also keeps little signed thinking across the boundary (findings §46).
|
|
9
|
+
*
|
|
10
|
+
* pi's own cut is moved to the last assistant message that reaches the
|
|
11
|
+
* provider. When pi cut earlier (a keep window above 0), the messages between
|
|
12
|
+
* the two cuts join the doomed span. When pi cut later (with the window at 0,
|
|
13
|
+
* pi cuts at the last cut point, which can be a queued prompt or a harness
|
|
14
|
+
* notification that arrived after the reply), the reply and what follows it
|
|
15
|
+
* leave the doomed span, so the summary does not repeat what stays verbatim.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { latestCompaction } from "../lib/compaction-boundary.ts";
|
|
19
|
+
|
|
20
|
+
/** The fields of a session entry the cut reads; pi's `SessionEntry` fits it. */
|
|
21
|
+
export type CutEntry = {
|
|
22
|
+
id: string;
|
|
23
|
+
type: string;
|
|
24
|
+
message?: { role?: string; stopReason?: string; content?: unknown };
|
|
25
|
+
targetId?: string;
|
|
26
|
+
replacement?: { content: unknown } | null;
|
|
27
|
+
firstKeptEntryId?: string | null;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type Span<M> = { messagesToSummarize: M[]; turnPrefixMessages: M[]; isSplitTurn: boolean };
|
|
31
|
+
type Preparation<M> = Span<M> & { firstKeptEntryId: string };
|
|
32
|
+
|
|
33
|
+
/** What pi summarizes for a preparation: the messages before the cut, a split turn's prefix included. */
|
|
34
|
+
export function doomedMessages<M>(preparation: Span<M>): M[] {
|
|
35
|
+
return preparation.isSplitTurn ? [...preparation.messagesToSummarize, ...preparation.turnPrefixMessages] : preparation.messagesToSummarize;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* pi's preparation moved to Claude Code's cut, with the entry the kept tail
|
|
40
|
+
* starts at. pi's own when its cut already is the reply, no reply is still in
|
|
41
|
+
* context, or the messages to take off are not the doomed span's tail.
|
|
42
|
+
*/
|
|
43
|
+
export function claudeCodeCut<E extends CutEntry, M extends { role: string }, P extends Preparation<M>>(
|
|
44
|
+
entries: readonly E[],
|
|
45
|
+
preparation: P,
|
|
46
|
+
toMessages: (entry: E) => M[],
|
|
47
|
+
): { preparation: P; firstKeptEntryId: string } {
|
|
48
|
+
const unchanged = { preparation, firstKeptEntryId: preparation.firstKeptEntryId };
|
|
49
|
+
const cut = lastReplyCut(entries, preparation.firstKeptEntryId);
|
|
50
|
+
if (!cut) return unchanged;
|
|
51
|
+
const moved = moveCut(preparation, spanMessages(cut.span, entries, toMessages), cut.later);
|
|
52
|
+
return moved ? { preparation: moved, firstKeptEntryId: cut.firstKeptEntryId } : unchanged;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The entry Claude Code's cut keeps from, and the entries between it and pi's
|
|
57
|
+
* cut: summarized too when the reply is `later` than pi's cut, kept when it is
|
|
58
|
+
* earlier. Undefined when pi's entry is not on the branch, no reply is still
|
|
59
|
+
* in context, or pi already cut at the reply.
|
|
60
|
+
*
|
|
61
|
+
* An assistant message that ended in an error or an abort never reaches the
|
|
62
|
+
* provider (pi-ai's `transformMessages` drops it), and one whose content (as
|
|
63
|
+
* a context edit leaves it) has no text or tool call is omitted or skipped by
|
|
64
|
+
* its converter, so none of them is a reply to keep. A reply an
|
|
65
|
+
* earlier compaction already summarized is out of reach.
|
|
66
|
+
*/
|
|
67
|
+
export function lastReplyCut<E extends CutEntry>(
|
|
68
|
+
entries: readonly E[],
|
|
69
|
+
piFirstKeptEntryId: string,
|
|
70
|
+
): { firstKeptEntryId: string; span: E[]; later: boolean } | undefined {
|
|
71
|
+
const piIndex = entries.findIndex((entry) => entry.id === piFirstKeptEntryId);
|
|
72
|
+
if (piIndex < 0) return undefined;
|
|
73
|
+
const edits = contextEdits(entries);
|
|
74
|
+
const start = latestCompaction(entries)?.keptStart ?? 0;
|
|
75
|
+
let replyIndex = -1;
|
|
76
|
+
for (let i = entries.length - 1; i >= start; i--) {
|
|
77
|
+
const entry = entries[i];
|
|
78
|
+
const message = entry.message;
|
|
79
|
+
if (entry.type !== "message" || message?.role !== "assistant") continue;
|
|
80
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") continue;
|
|
81
|
+
// The content the provider gets: a context edit's replacement, or none when it omits the message.
|
|
82
|
+
const content = edits.has(entry.id) ? edits.get(entry.id)?.content : message.content;
|
|
83
|
+
if (!sendsContent(content)) continue;
|
|
84
|
+
replyIndex = i;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (replyIndex < 0 || replyIndex === piIndex) return undefined;
|
|
88
|
+
const later = replyIndex > piIndex;
|
|
89
|
+
return {
|
|
90
|
+
firstKeptEntryId: entries[replyIndex].id,
|
|
91
|
+
span: later ? entries.slice(piIndex, replyIndex) : entries.slice(replyIndex, piIndex),
|
|
92
|
+
later,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Each edited entry's replacement (null when the edit omits it), the latest edit winning, as in pi's projection. */
|
|
97
|
+
function contextEdits(entries: readonly CutEntry[]): Map<string, CutEntry["replacement"]> {
|
|
98
|
+
const edits = new Map<string, CutEntry["replacement"]>();
|
|
99
|
+
for (const entry of entries) if (entry.type === "context_edit" && entry.targetId) edits.set(entry.targetId, entry.replacement);
|
|
100
|
+
return edits;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Whether an assistant message carries text or a tool call (thinking alone is not a reply). */
|
|
104
|
+
function sendsContent(content: unknown): boolean {
|
|
105
|
+
if (typeof content === "string") return content.trim().length > 0;
|
|
106
|
+
return (
|
|
107
|
+
Array.isArray(content) &&
|
|
108
|
+
content.some((block: { type?: string; text?: string }) => block?.type === "toolCall" || (block?.type === "text" && !!block.text?.trim()))
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The context messages of entries in branch order, as pi projects them: an
|
|
114
|
+
* omitted entry contributes nothing, a replaced one its replacement content
|
|
115
|
+
* (`buildSessionProjection` in pi 0.87 and later; earlier pi has no edits), and
|
|
116
|
+
* an older compaction entry inside the span nothing (only the newest
|
|
117
|
+
* compaction's summary is context).
|
|
118
|
+
*/
|
|
119
|
+
export function spanMessages<E extends CutEntry, M extends { role: string }>(span: readonly E[], all: readonly E[], toMessages: (entry: E) => M[]): M[] {
|
|
120
|
+
const edits = contextEdits(all);
|
|
121
|
+
return span.flatMap((entry) => {
|
|
122
|
+
if (entry.type === "compaction" || entry.type === "context_edit") return [];
|
|
123
|
+
const messages = toMessages(entry);
|
|
124
|
+
if (!edits.has(entry.id)) return messages;
|
|
125
|
+
const replacement = edits.get(entry.id);
|
|
126
|
+
if (!replacement) return [];
|
|
127
|
+
return messages.map((message) => {
|
|
128
|
+
if (!["user", "assistant", "toolResult", "custom"].includes(message.role)) return message;
|
|
129
|
+
const content =
|
|
130
|
+
(message.role === "assistant" || message.role === "toolResult") && typeof replacement.content === "string"
|
|
131
|
+
? [{ type: "text", text: replacement.content }]
|
|
132
|
+
: replacement.content;
|
|
133
|
+
return { ...message, content };
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* pi's preparation with the doomed span moved to the new cut, the split turn's
|
|
140
|
+
* prefix folded in: `messages` appended when the reply is `later` than pi's
|
|
141
|
+
* cut, else taken off the span's end. Undefined when those are not the span's
|
|
142
|
+
* tail (role and timestamp, message by message) or would leave nothing to
|
|
143
|
+
* summarize: pi's cut stays.
|
|
144
|
+
*/
|
|
145
|
+
export function moveCut<P extends Span<unknown>>(preparation: P, messages: P["messagesToSummarize"], later: boolean): P | undefined {
|
|
146
|
+
const doomed = doomedMessages(preparation);
|
|
147
|
+
const moved = (messagesToSummarize: P["messagesToSummarize"]): P => ({ ...preparation, messagesToSummarize, turnPrefixMessages: [], isSplitTurn: false });
|
|
148
|
+
if (later) return moved([...doomed, ...messages]);
|
|
149
|
+
const keep = doomed.length - messages.length;
|
|
150
|
+
if (keep <= 0) return undefined;
|
|
151
|
+
const same = (a: unknown, b: unknown) => {
|
|
152
|
+
const [x, y] = [a as { role?: string; timestamp?: number } | null, b as { role?: string; timestamp?: number } | null];
|
|
153
|
+
return x?.role === y?.role && x?.timestamp === y?.timestamp;
|
|
154
|
+
};
|
|
155
|
+
if (messages.some((message, i) => !same(message, doomed[keep + i]))) return undefined;
|
|
156
|
+
return moved(doomed.slice(0, keep));
|
|
157
|
+
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* compaction extension — replaces pi's summarizer with Claude Code's.
|
|
3
3
|
*
|
|
4
4
|
* Hooks `session_before_compact` (manual /compact, threshold, and overflow
|
|
5
|
-
* alike),
|
|
6
|
-
* compaction instruction; only the <summary> block of the
|
|
7
|
-
* context.
|
|
5
|
+
* alike), moves pi's cut point to the last assistant reply (`cut.ts`), and
|
|
6
|
+
* sends Claude Code's compaction instruction; only the <summary> block of the
|
|
7
|
+
* reply survives as context, followed by that reply.
|
|
8
8
|
*
|
|
9
9
|
* The call is built to hit the provider prompt cache, which is why Claude
|
|
10
10
|
* Code compacts on the *session model* rather than something cheaper: it
|
|
@@ -41,20 +41,25 @@ import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
|
41
41
|
import {
|
|
42
42
|
convertToLlm,
|
|
43
43
|
type ExtensionAPI,
|
|
44
|
-
type ExtensionContext,
|
|
45
44
|
type SessionBeforeCompactEvent,
|
|
45
|
+
type SessionEntry,
|
|
46
|
+
sessionEntryToContextMessages,
|
|
46
47
|
} from "@earendil-works/pi-coding-agent";
|
|
47
48
|
import {
|
|
48
49
|
type AnthropicModelCompat,
|
|
49
|
-
anthropicBetas,
|
|
50
50
|
clearThinkingApplies,
|
|
51
51
|
clearThinkingEnabled,
|
|
52
|
-
|
|
52
|
+
sessionRequestHeaders,
|
|
53
53
|
withClearThinking,
|
|
54
54
|
} from "../context-management/index.ts";
|
|
55
|
+
import { PRECOMPACT_INSTRUCTIONS_CHANNEL, type PreCompactInstructions } from "../hooks/compaction.ts";
|
|
55
56
|
import { looksLikeAnthropicRequest } from "../lib/anthropic-payload.ts";
|
|
56
57
|
import { forcedReasoningLevel } from "../lib/model-policy.ts";
|
|
58
|
+
import { captureRequest, extendPayload, LAST_REQUEST_CHANNEL, LastExchange, lastCovered, type RequestCapture, replayOutputCap } from "../lib/request-replay.ts";
|
|
59
|
+
import { withoutSystemMessages } from "../lib/side-call.ts";
|
|
60
|
+
import { claudeCodeCut, type CutEntry, doomedMessages } from "./cut.ts";
|
|
57
61
|
import { fitToBudget, replayFits, withoutUsage } from "./fit.ts";
|
|
62
|
+
import { withoutThinking } from "./kept-thinking.ts";
|
|
58
63
|
import { buildCompactionInstruction, COMPACTION_MAX_TOKENS, continuationSummary, extractSummary } from "./prompt.ts";
|
|
59
64
|
|
|
60
65
|
/** Compaction reads a whole context window; give it more room than the classifier's 30s. */
|
|
@@ -71,6 +76,11 @@ export const STANDALONE_SYSTEM_PROMPT =
|
|
|
71
76
|
type Preparation = Pick<SessionBeforeCompactEvent["preparation"], "messagesToSummarize" | "turnPrefixMessages" | "isSplitTurn" | "previousSummary">;
|
|
72
77
|
|
|
73
78
|
export default function compactionExtension(pi: ExtensionAPI) {
|
|
79
|
+
const hookInstructions = new WeakMap<AbortSignal, string>();
|
|
80
|
+
pi.events.on(PRECOMPACT_INSTRUCTIONS_CHANNEL, (data) => {
|
|
81
|
+
const { signal, instructions } = data as PreCompactInstructions;
|
|
82
|
+
hookInstructions.set(signal, instructions);
|
|
83
|
+
});
|
|
74
84
|
/**
|
|
75
85
|
* The exact message array the session last handed the provider, captured
|
|
76
86
|
* from the `context` event AFTER every extension has mutated it — most
|
|
@@ -87,38 +97,83 @@ export default function compactionExtension(pi: ExtensionAPI) {
|
|
|
87
97
|
* standalone path) diverges at message one, because those injected reminders
|
|
88
98
|
* never become entries — so it cannot reproduce the cached prefix at all.
|
|
89
99
|
*
|
|
90
|
-
* We hold the
|
|
91
|
-
* structuredClone the session never mutates again, and
|
|
92
|
-
* last extension with a `context` handler, so
|
|
93
|
-
*
|
|
100
|
+
* We hold the messages themselves, not copies: `emitContext` hands each
|
|
101
|
+
* handler a fresh structuredClone the session never mutates again, and
|
|
102
|
+
* compaction is the last extension with a `context` handler, so these are
|
|
103
|
+
* exactly the messages the turn sent. Returning nothing keeps the handler a
|
|
104
|
+
* pure observer.
|
|
105
|
+
*
|
|
106
|
+
* The system prompt is captured at the same moment. `ctx.getSystemPrompt()`
|
|
107
|
+
* reflects the prompt a run forced through `before_agent_start` only while
|
|
108
|
+
* that run lasts; pi drops the run's options when it settles, so read from an
|
|
109
|
+
* idle `/compact` it is pi's default prompt and the replay misses the cache
|
|
110
|
+
* (findings §27).
|
|
111
|
+
*/
|
|
112
|
+
let captured: { messages: AgentMessage[]; systemPrompt: string; model: string | undefined } | undefined;
|
|
113
|
+
pi.on("context", (event, ctx) => {
|
|
114
|
+
captured = { messages: event.messages, systemPrompt: ctx.getSystemPrompt(), model: modelKey(ctx.model) };
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The same request as pi put it on the wire, and the reply that answered it
|
|
119
|
+
* (lib/request-replay.ts). This extension loads after every extension that
|
|
120
|
+
* edits the payload (tool-search, context-management), so the body it sees
|
|
121
|
+
* is final; it is published for the btw and recap side calls too.
|
|
94
122
|
*/
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
123
|
+
const exchange = new LastExchange<AgentMessage & { role: string }>();
|
|
124
|
+
const publish = (capture: ReturnType<typeof captureRequest>) => {
|
|
125
|
+
exchange.setCapture(capture);
|
|
126
|
+
pi.events.emit(LAST_REQUEST_CHANNEL, capture);
|
|
127
|
+
};
|
|
128
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
129
|
+
// The context this body was built from ends where a fork's own messages start.
|
|
130
|
+
const covers = captured ? lastCovered(captured.messages as { role: string; timestamp?: number }[]) : undefined;
|
|
131
|
+
publish(captureRequest(ctx.model as { api?: string; provider?: string; id?: string } | undefined, event.payload, covers, ctx.sessionManager.getSessionId()));
|
|
132
|
+
});
|
|
133
|
+
pi.on("message_end", (event) => {
|
|
134
|
+
exchange.noteMessage(event.message as AgentMessage & { role: string });
|
|
135
|
+
});
|
|
136
|
+
pi.on("session_start", () => {
|
|
137
|
+
captured = undefined;
|
|
138
|
+
publish(undefined);
|
|
98
139
|
});
|
|
140
|
+
|
|
99
141
|
// After a compaction the capture describes the pre-compaction request. A
|
|
100
142
|
// second /compact before any turn would otherwise re-summarize history the
|
|
101
143
|
// first one already folded away (and mis-scope the kept tail); the standalone
|
|
102
144
|
// reconstruction from entries serves until the next request recaptures.
|
|
103
145
|
pi.on("session_compact", () => {
|
|
104
|
-
|
|
146
|
+
captured = undefined;
|
|
147
|
+
publish(undefined);
|
|
105
148
|
});
|
|
106
149
|
// A /tree branch switch stays in the same process and fires no `context`
|
|
107
150
|
// event (navigateTree rebuilds the messages itself), so the capture would
|
|
108
151
|
// still describe the abandoned branch — and a /compact before the next turn
|
|
109
152
|
// summarized work the kept branch never did (review H1). Same remedy.
|
|
110
153
|
pi.on("session_tree", () => {
|
|
111
|
-
|
|
154
|
+
captured = undefined;
|
|
155
|
+
publish(undefined);
|
|
112
156
|
});
|
|
113
157
|
|
|
114
158
|
pi.on("session_before_compact", async (event, ctx) => {
|
|
159
|
+
const instructions = hookInstructions.get(event.signal);
|
|
160
|
+
hookInstructions.delete(event.signal);
|
|
161
|
+
if (instructions) event = { ...event, customInstructions: [event.customInstructions, instructions].filter(Boolean).join("\n\n") };
|
|
115
162
|
if (process.env.CC_COMPACTION === "0") return undefined;
|
|
116
163
|
|
|
117
164
|
const model = ctx.model;
|
|
118
165
|
if (!model) return undefined;
|
|
119
166
|
// Snapshot before the first await so the capture and pi's preparation
|
|
120
167
|
// describe the same request even if a context event lands meanwhile.
|
|
121
|
-
|
|
168
|
+
// A replay exists to read the session model's cache, and the prompt is built
|
|
169
|
+
// per model, so a capture made under another model (a `/model` switch with
|
|
170
|
+
// no turn since) is not replayed: the standalone shape serves.
|
|
171
|
+
const capture =
|
|
172
|
+
captured && captured.model === modelKey(model) ? { ...captured, messages: withoutSystemMessages(captured.messages) } : undefined;
|
|
173
|
+
const wire = exchange.forModel(model);
|
|
174
|
+
// Claude Code's cut: keep only the last assistant reply, and summarize
|
|
175
|
+
// exactly what lies before it (cut.ts).
|
|
176
|
+
const { preparation, firstKeptEntryId } = claudeCodeCut(event.branchEntries as (SessionEntry & CutEntry)[], event.preparation, sessionEntryToContextMessages);
|
|
122
177
|
|
|
123
178
|
try {
|
|
124
179
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
@@ -133,16 +188,24 @@ export default function compactionExtension(pi: ExtensionAPI) {
|
|
|
133
188
|
// Otherwise (no capture yet, an overflow, a nearly full window) the
|
|
134
189
|
// standalone shape summarizes the doomed span pi isolated. An overflow
|
|
135
190
|
// never replays, so its replay request is not even built.
|
|
136
|
-
const replay =
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
191
|
+
const replay = capture && event.reason !== "overflow" ? replayRequest(pi, capture, event, preparation, wire?.reply, model.api) : undefined;
|
|
192
|
+
const replaying = replay !== undefined && replayFits(event.reason, model, replay, maxTokens);
|
|
193
|
+
const request = replaying
|
|
194
|
+
? replay
|
|
195
|
+
: standaloneRequest(
|
|
196
|
+
model,
|
|
197
|
+
preparation,
|
|
198
|
+
buildCompactionInstruction({ reason: event.reason, customInstructions: event.customInstructions }),
|
|
199
|
+
maxTokens,
|
|
200
|
+
);
|
|
201
|
+
// A replay goes out as the session's own last request body with the
|
|
202
|
+
// reply and the instruction appended, so it reads the cache that request
|
|
203
|
+
// wrote: rebuilt from `request`, the tools array (tool-search reshapes it
|
|
204
|
+
// on the wire) never matched on first-party Claude (measured on Sonnet 5:
|
|
205
|
+
// 0 read, 65k written). `request` still sizes the call (fit.ts), and
|
|
206
|
+
// pi-ai converts only the tail. No capture for this model, or too little
|
|
207
|
+
// room left under the captured cap: `request` as is.
|
|
208
|
+
const plan = replaying && wire ? wirePlan(wire, request, maxTokens) : undefined;
|
|
146
209
|
|
|
147
210
|
// When context-management (clear_thinking) is active for this session,
|
|
148
211
|
// the agent loop's cached message prefix has old thinking blocks cleared
|
|
@@ -159,17 +222,32 @@ export default function compactionExtension(pi: ExtensionAPI) {
|
|
|
159
222
|
// the Anthropic-shaped fields, and are gated to Anthropic anyway.
|
|
160
223
|
const cmModel = model as { api?: string; provider?: string; baseUrl?: string; compat?: AnthropicModelCompat };
|
|
161
224
|
const clearThinking = clearThinkingEnabled(process.env.CC_CLEAR_THINKING, cmModel);
|
|
162
|
-
const headers =
|
|
163
|
-
|
|
164
|
-
|
|
225
|
+
const headers = sessionRequestHeaders(cmModel, auth.headers);
|
|
226
|
+
|
|
227
|
+
// A replay sends the captured body (which already carries the
|
|
228
|
+
// clear_thinking edit) with the tail pi-ai just built appended.
|
|
229
|
+
// Otherwise the clear_thinking body edit, matching the session's
|
|
230
|
+
// requests (see the header note above), only on Anthropic requests
|
|
231
|
+
// that carry thinking, exactly as the context-management extension gates it.
|
|
232
|
+
let onPayload: ((payload: unknown) => unknown) | undefined;
|
|
233
|
+
if (plan) {
|
|
234
|
+
onPayload = (payload) => extendPayload(plan.capture, payload as Record<string, unknown>, plan.maxTokens);
|
|
235
|
+
} else if (clearThinking) {
|
|
236
|
+
onPayload = (payload) =>
|
|
237
|
+
looksLikeAnthropicRequest(payload) &&
|
|
238
|
+
clearThinkingApplies(payload as Record<string, unknown>, cmModel.compat?.forceAdaptiveThinking === true)
|
|
239
|
+
? withClearThinking(payload as Record<string, unknown>)
|
|
240
|
+
: payload;
|
|
241
|
+
}
|
|
165
242
|
|
|
166
243
|
const timeout = AbortSignal.timeout(COMPACTION_TIMEOUT_MS);
|
|
167
|
-
const
|
|
244
|
+
const callContext: Context = plan ? { systemPrompt: "", messages: plan.tail, tools: [] } : request;
|
|
245
|
+
const result = await completeSimple(baseUrl ? ({ ...model, baseUrl } as Model<Api>) : model, callContext, {
|
|
168
246
|
apiKey: auth.apiKey,
|
|
169
247
|
headers,
|
|
170
248
|
env: auth.env,
|
|
171
249
|
signal: AbortSignal.any([event.signal, timeout]),
|
|
172
|
-
maxTokens,
|
|
250
|
+
maxTokens: plan?.maxTokens ?? maxTokens,
|
|
173
251
|
// The session id is the prompt-cache key / routing affinity on the
|
|
174
252
|
// providers that use one (openai-codex prompt_cache_key, session
|
|
175
253
|
// headers). Without it the replayed prefix cannot hit the session's
|
|
@@ -189,18 +267,13 @@ export default function compactionExtension(pi: ExtensionAPI) {
|
|
|
189
267
|
// unless the model cannot disable thinking, where an off-request
|
|
190
268
|
// would 400 (forcedReasoningLevel sends its lowest level instead).
|
|
191
269
|
reasoning: ctx.thinkingLevel === "off" ? forcedReasoningLevel(model) : ctx.thinkingLevel,
|
|
192
|
-
|
|
193
|
-
// the header note above). Only attached on Anthropic requests that
|
|
194
|
-
// carry thinking, exactly as the context-management extension gates it.
|
|
195
|
-
onPayload: clearThinking
|
|
196
|
-
? (payload: unknown) =>
|
|
197
|
-
looksLikeAnthropicRequest(payload) &&
|
|
198
|
-
clearThinkingApplies(payload as Record<string, unknown>, cmModel.compat?.forceAdaptiveThinking === true)
|
|
199
|
-
? withClearThinking(payload as Record<string, unknown>)
|
|
200
|
-
: payload
|
|
201
|
-
: undefined,
|
|
270
|
+
onPayload,
|
|
202
271
|
});
|
|
203
272
|
|
|
273
|
+
// A failed or capped stream can carry partial text. Never replace the
|
|
274
|
+
// conversation with that incomplete checkpoint; let pi retry its summary.
|
|
275
|
+
if (result.stopReason === "error" || result.stopReason === "aborted" || result.stopReason === "length") return undefined;
|
|
276
|
+
|
|
204
277
|
const text = result.content
|
|
205
278
|
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
206
279
|
.map((block) => block.text)
|
|
@@ -212,7 +285,7 @@ export default function compactionExtension(pi: ExtensionAPI) {
|
|
|
212
285
|
compaction: {
|
|
213
286
|
// Undefined in --no-session runs; the pointer line is dropped.
|
|
214
287
|
summary: continuationSummary(summary, ctx.sessionManager.getSessionFile()),
|
|
215
|
-
firstKeptEntryId
|
|
288
|
+
firstKeptEntryId,
|
|
216
289
|
tokensBefore: event.preparation.tokensBefore,
|
|
217
290
|
usage: result.usage,
|
|
218
291
|
},
|
|
@@ -224,20 +297,50 @@ export default function compactionExtension(pi: ExtensionAPI) {
|
|
|
224
297
|
});
|
|
225
298
|
}
|
|
226
299
|
|
|
300
|
+
/** A model's identity for matching a capture to the current model. */
|
|
301
|
+
function modelKey(model: { provider?: string; id?: string } | undefined): string | undefined {
|
|
302
|
+
return model ? `${model.provider}/${model.id}` : undefined;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* A replay's wire form: the tail pi-ai converts (the reply, if any, and the
|
|
307
|
+
* instruction: the last messages of `request`) and the output cap left under
|
|
308
|
+
* the captured request's own, or undefined when too little room is left.
|
|
309
|
+
*/
|
|
310
|
+
function wirePlan(
|
|
311
|
+
wire: { capture: RequestCapture; reply: unknown },
|
|
312
|
+
request: Context,
|
|
313
|
+
maxTokens: number,
|
|
314
|
+
): { capture: RequestCapture; tail: Message[]; maxTokens: number } | undefined {
|
|
315
|
+
const tail = request.messages.slice(-(wire.reply ? 2 : 1));
|
|
316
|
+
const cap = replayOutputCap(wire.capture, tail, maxTokens);
|
|
317
|
+
return cap === undefined ? undefined : { capture: wire.capture, tail, maxTokens: cap };
|
|
318
|
+
}
|
|
319
|
+
|
|
227
320
|
/**
|
|
228
321
|
* The cache-aligned shape: the session's system prompt, the active tool
|
|
229
322
|
* definitions in their active order — kept purely so the cached prefix (tools
|
|
230
323
|
* come first) still matches; a mismatch costs cache hits, never correctness —
|
|
231
324
|
* the captured request verbatim, and the instruction as one more user message.
|
|
232
325
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* names it (`keptTailOf`). Cutting the tail off the request instead was
|
|
326
|
+
* The kept tail (the last assistant reply and anything after it, `cut.ts`)
|
|
327
|
+
* stays verbatim after the summary; the captured request still carries it, so
|
|
328
|
+
* the instruction names it (`keptTailOf`). Cutting the tail off the request instead was
|
|
236
329
|
* rejected: Anthropic checks cache hits only ~20 blocks back from the
|
|
237
330
|
* breakpoint, so a request ending well before the last cached block misses
|
|
238
331
|
* the cache the whole replay exists to hit.
|
|
239
332
|
*/
|
|
240
|
-
function replayRequest(
|
|
333
|
+
function replayRequest(
|
|
334
|
+
pi: ExtensionAPI,
|
|
335
|
+
captured: { messages: AgentMessage[]; systemPrompt: string },
|
|
336
|
+
event: SessionBeforeCompactEvent,
|
|
337
|
+
preparation: Preparation,
|
|
338
|
+
reply: AgentMessage | undefined,
|
|
339
|
+
api: string | undefined,
|
|
340
|
+
): Context {
|
|
341
|
+
// The reply that answered the captured request is part of the conversation
|
|
342
|
+
// pi is compacting, and the last message the replayed body gains.
|
|
343
|
+
const conversation = reply ? [...captured.messages, reply] : captured.messages;
|
|
241
344
|
const byName = new Map(pi.getAllTools().map((tool) => [tool.name, tool]));
|
|
242
345
|
const tools = pi
|
|
243
346
|
.getActiveTools()
|
|
@@ -247,25 +350,38 @@ function replayRequest(pi: ExtensionAPI, ctx: ExtensionContext, captured: AgentM
|
|
|
247
350
|
const instruction = buildCompactionInstruction({
|
|
248
351
|
reason: event.reason,
|
|
249
352
|
customInstructions: event.customInstructions,
|
|
250
|
-
keptTail: keptTailOf(
|
|
353
|
+
keptTail: keptTailOf(conversation, preparation, api),
|
|
251
354
|
});
|
|
252
|
-
return { systemPrompt:
|
|
355
|
+
return { systemPrompt: captured.systemPrompt, messages: [...convertToLlm(conversation), instructionMessage(instruction)], tools };
|
|
253
356
|
}
|
|
254
357
|
|
|
255
358
|
/**
|
|
256
359
|
* The standalone shape: the doomed span reconstructed from session entries,
|
|
257
|
-
* its assistant usage zeroed
|
|
258
|
-
*
|
|
259
|
-
*
|
|
360
|
+
* its assistant usage zeroed, its signed thinking removed (it would fail the
|
|
361
|
+
* check under this system prompt; `kept-thinking.ts`), and its largest tool
|
|
362
|
+
* results cleared until pi's clamp leaves room for the summary (`fit.ts`), no
|
|
363
|
+
* tools, a one-line system prompt. Gets no kept-tail note: it holds only the doomed span. If it fails,
|
|
260
364
|
* pi's own compaction serves. Exported for the unit test.
|
|
261
365
|
*/
|
|
262
366
|
export function standaloneRequest(model: Model<Api>, preparation: Preparation, instruction: string, maxTokens: number): Context {
|
|
263
|
-
const
|
|
367
|
+
const span = withoutThinking(withoutUsage(convertToLlm(reconstructFromEntries(preparation))), model.api);
|
|
368
|
+
const messages = [...span, instructionMessage(instruction)];
|
|
264
369
|
return fitToBudget(model, { systemPrompt: STANDALONE_SYSTEM_PROMPT, messages }, maxTokens).request;
|
|
265
370
|
}
|
|
266
371
|
|
|
267
372
|
const instructionMessage = (instruction: string): Message => ({ role: "user", content: instruction, timestamp: Date.now() });
|
|
268
373
|
|
|
374
|
+
/**
|
|
375
|
+
* APIs whose request puts a run of consecutive tool results into ONE user
|
|
376
|
+
* message (pi-ai's converters: Anthropic and Bedrock require it, Gemini merges
|
|
377
|
+
* function responses into one turn). The model counts messages on the wire, so
|
|
378
|
+
* the kept-tail note counts such a run once.
|
|
379
|
+
*/
|
|
380
|
+
const MERGES_TOOL_RESULTS = new Set(["anthropic-messages", "bedrock-converse-stream", "google-generative-ai", "google-vertex"]);
|
|
381
|
+
const GOOGLE_APIS = new Set(["google-generative-ai", "google-vertex"]);
|
|
382
|
+
|
|
383
|
+
type TailMessage = { role: string; content?: unknown; toolName?: unknown };
|
|
384
|
+
|
|
269
385
|
/**
|
|
270
386
|
* The verbatim-kept tail of the captured request: everything after the doomed
|
|
271
387
|
* span. pi builds the context as `[compactionSummary?] + one message per entry`
|
|
@@ -273,37 +389,64 @@ const instructionMessage = (instruction: string): Message => ({ role: "user", co
|
|
|
273
389
|
* `messagesToSummarize`/`turnPrefixMessages` with the same mapping, so the
|
|
274
390
|
* doomed span is a prefix of the captured array. Alignment is checked role by
|
|
275
391
|
* role; on any mismatch (an extension that inserted a message) no note is made
|
|
276
|
-
* rather than a wrong one.
|
|
392
|
+
* rather than a wrong one.
|
|
393
|
+
*
|
|
394
|
+
* `count` is in the provider's messages (`api`): a parallel batch's results
|
|
395
|
+
* are one message on Anthropic. `landmark` names the first kept message so the
|
|
396
|
+
* model need not count: how its text opens, else the tool it calls, else the
|
|
397
|
+
* tool whose result it is. Exported for the unit test.
|
|
277
398
|
*/
|
|
278
399
|
export function keptTailOf(
|
|
279
|
-
captured: readonly
|
|
400
|
+
captured: readonly TailMessage[],
|
|
280
401
|
preparation: Preparation,
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
...preparation.messagesToSummarize,
|
|
285
|
-
...(preparation.isSplitTurn ? preparation.turnPrefixMessages : []),
|
|
286
|
-
];
|
|
402
|
+
api?: string,
|
|
403
|
+
): { count: number; landmark?: string } | undefined {
|
|
404
|
+
const doomed: { role: string }[] = [...(preparation.previousSummary ? [{ role: "compactionSummary" }] : []), ...doomedMessages(preparation)];
|
|
287
405
|
if (doomed.length === 0 || doomed.length > captured.length) return undefined;
|
|
288
406
|
for (let i = 0; i < doomed.length; i++) {
|
|
289
407
|
if (captured[i].role !== doomed[i].role) return undefined;
|
|
290
408
|
}
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
293
|
-
|
|
409
|
+
const tail = captured.slice(doomed.length);
|
|
410
|
+
if (tail.length === 0) return { count: 0 };
|
|
411
|
+
// On Google a tool result's image can become a user message of its own,
|
|
412
|
+
// depending on the model: the count cannot be known, so no note.
|
|
413
|
+
const imageResult = tail.some((m) => m.role === "toolResult" && Array.isArray(m.content) && (m.content as { type?: string }[]).some((b) => b?.type === "image"));
|
|
414
|
+
if (imageResult && api !== undefined && GOOGLE_APIS.has(api)) return undefined;
|
|
415
|
+
const merges = api !== undefined && MERGES_TOOL_RESULTS.has(api);
|
|
416
|
+
const count = tail.filter((m, i) => !(merges && m.role === "toolResult" && tail[i - 1]?.role === "toolResult")).length;
|
|
417
|
+
return { count, landmark: landmarkOf(tail[0]) };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** How the note points at the first kept message, or undefined when nothing identifies it. */
|
|
421
|
+
function landmarkOf(message: TailMessage): string | undefined {
|
|
422
|
+
if (message.role === "toolResult" && typeof message.toolName === "string") return `the result of the ${message.toolName} call`;
|
|
423
|
+
const content = Array.isArray(message.content) ? (message.content as { type?: string; text?: string; name?: unknown; arguments?: unknown }[]) : [];
|
|
424
|
+
const text = typeof message.content === "string" ? message.content : content.find((b) => b?.type === "text" && b.text?.trim())?.text;
|
|
425
|
+
const opening = oneLine(text);
|
|
426
|
+
if (opening) return `the message that opens "${opening}"`;
|
|
427
|
+
const call = content.find((b) => b?.type === "toolCall" && typeof b.name === "string");
|
|
428
|
+
if (message.role === "assistant" && call) {
|
|
429
|
+
const hint = oneLine(callHint(call.arguments));
|
|
430
|
+
return `the assistant message that calls ${call.name as string}${hint ? ` (${hint})` : ""}`;
|
|
431
|
+
}
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/** The argument that says what a call is about: its path, command or pattern, else its first string argument. */
|
|
436
|
+
function callHint(args: unknown): string | undefined {
|
|
437
|
+
if (!args || typeof args !== "object") return undefined;
|
|
438
|
+
const record = args as Record<string, unknown>;
|
|
439
|
+
for (const key of ["path", "file_path", "command", "pattern", "url", "query"]) {
|
|
440
|
+
if (typeof record[key] === "string") return record[key] as string;
|
|
441
|
+
}
|
|
442
|
+
return Object.values(record).find((value): value is string => typeof value === "string");
|
|
294
443
|
}
|
|
295
444
|
|
|
296
|
-
/** The first ~80 characters of a
|
|
297
|
-
function
|
|
298
|
-
const content = message.content;
|
|
299
|
-
const text =
|
|
300
|
-
typeof content === "string"
|
|
301
|
-
? content
|
|
302
|
-
: Array.isArray(content)
|
|
303
|
-
? (content.find((b) => b && typeof b === "object" && (b as { type?: string }).type === "text") as { text?: string } | undefined)?.text
|
|
304
|
-
: undefined;
|
|
445
|
+
/** The first ~80 characters of a text, single-line, for a landmark. */
|
|
446
|
+
function oneLine(text: string | undefined): string | undefined {
|
|
305
447
|
if (!text) return undefined;
|
|
306
448
|
const line = text.replace(/\s+/g, " ").trim();
|
|
449
|
+
if (!line) return undefined;
|
|
307
450
|
return line.length > 80 ? `${line.slice(0, 80)}…` : line;
|
|
308
451
|
}
|
|
309
452
|
|
|
@@ -319,9 +462,7 @@ function openingText(message: { content?: unknown }): string | undefined {
|
|
|
319
462
|
* summarized with the rest.
|
|
320
463
|
*/
|
|
321
464
|
function reconstructFromEntries(preparation: Preparation): AgentMessage[] {
|
|
322
|
-
const doomed = preparation
|
|
323
|
-
? [...preparation.messagesToSummarize, ...preparation.turnPrefixMessages]
|
|
324
|
-
: preparation.messagesToSummarize;
|
|
465
|
+
const doomed = doomedMessages(preparation);
|
|
325
466
|
return preparation.previousSummary
|
|
326
467
|
? [
|
|
327
468
|
{
|