@pi-unipi/unipi 3.0.0-alpha.2 → 3.0.0-alpha.21
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 +292 -0
- package/README.md +8 -11
- package/docs/prefix-cache-architecture.md +14 -0
- package/package.json +22 -24
- package/packages/ask-user/README.md +55 -107
- package/packages/ask-user/ask-ui.ts +306 -732
- package/packages/ask-user/clipboard.ts +50 -0
- package/packages/ask-user/config.ts +74 -78
- package/packages/ask-user/package.json +2 -2
- package/packages/ask-user/questions.ts +151 -0
- package/packages/ask-user/skills/ask-user/SKILL.md +59 -133
- package/packages/ask-user/tools.ts +198 -432
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +38 -43
- package/packages/autocomplete/src/sorting.ts +8 -0
- package/packages/background-tasks/package.json +2 -2
- package/packages/background-tasks/src/config.ts +0 -73
- package/packages/background-tasks/src/index.ts +20 -16
- package/packages/background-tasks/src/registry.ts +1 -171
- package/packages/background-tasks/src/types.ts +0 -32
- package/packages/btw/README.md +13 -56
- package/packages/btw/extensions/btw.ts +403 -1796
- package/packages/btw/package.json +5 -2
- package/packages/btw/skills/btw/SKILL.md +7 -143
- package/packages/compactor/README.md +56 -287
- package/packages/compactor/package.json +5 -9
- package/packages/compactor/skills/compactor/SKILL.md +13 -20
- package/packages/compactor/src/card.ts +90 -0
- package/packages/compactor/src/commands/index.ts +73 -130
- package/packages/compactor/src/compaction/compact-args.ts +5 -3
- package/packages/compactor/src/compaction/filter-noise.ts +1 -1
- package/packages/compactor/src/compaction/hooks.ts +487 -311
- package/packages/compactor/src/compaction/source.ts +178 -0
- package/packages/compactor/src/compaction/summarize.ts +586 -52
- package/packages/compactor/src/config/manager.ts +130 -127
- package/packages/compactor/src/config/schema.ts +32 -48
- package/packages/compactor/src/index.ts +40 -431
- package/packages/compactor/src/info-screen.ts +44 -190
- package/packages/compactor/src/session/recall-blocks.ts +1 -1
- package/packages/compactor/src/stats.ts +76 -0
- package/packages/compactor/src/tools/context-budget.ts +17 -34
- package/packages/compactor/src/tools/ctx-doctor.ts +45 -36
- package/packages/compactor/src/tools/register.ts +29 -269
- package/packages/compactor/src/types.ts +47 -186
- package/packages/core/README.md +25 -0
- package/packages/core/child-agent.ts +1 -0
- package/packages/core/command-echo.ts +63 -0
- package/packages/core/compaction-context.ts +41 -0
- package/packages/core/constants.ts +8 -38
- package/packages/core/events.ts +0 -53
- package/packages/core/fusion-status.ts +4 -0
- package/packages/core/global.d.ts +10 -0
- package/packages/core/harness-messages.ts +392 -0
- package/packages/core/index.ts +14 -0
- package/packages/core/kanboard-status.ts +49 -0
- package/packages/core/long-horizon-owner-status.ts +46 -0
- package/packages/core/model-cache.ts +81 -7
- package/packages/core/package.json +6 -3
- package/packages/core/src/attach/detect.ts +163 -0
- package/packages/core/src/child-agent/child-args.ts +31 -0
- package/packages/core/src/child-agent/delivery.ts +41 -0
- package/packages/core/src/child-agent/index.ts +5 -0
- package/packages/core/src/child-agent/readers.ts +203 -0
- package/packages/core/src/child-agent/runtime.ts +584 -0
- package/packages/core/src/evidence.ts +78 -0
- package/packages/core/src/hints/browser.ts +278 -0
- package/packages/core/src/hints/crab-data.ts +19 -0
- package/packages/core/src/hints/index.ts +865 -0
- package/packages/core/src/hints/lines.ts +607 -0
- package/packages/core/src/hints/store.ts +120 -0
- package/packages/core/src/jev/chatter.ts +24 -0
- package/packages/core/src/jev/settings.ts +177 -14
- package/packages/core/src/package-colors.ts +2 -0
- package/packages/core/src/settings/catalog.ts +39 -15
- package/packages/core/src/settings/engine.ts +48 -2
- package/packages/core/src/settings/hub.ts +83 -20
- package/packages/core/src/settings/open.ts +49 -0
- package/packages/core/src/settings/schema.ts +27 -7
- package/packages/core/src/tui/hub-kit.ts +18 -5
- package/packages/core/src/tui/kit.ts +198 -0
- package/packages/core/src/tui/progress.ts +73 -0
- package/packages/core/src/turn/arbiter.ts +290 -0
- package/packages/footer/README.md +4 -6
- package/packages/footer/package.json +3 -3
- package/packages/footer/src/glance-editor.ts +19 -11
- package/packages/footer/src/help.ts +1 -1
- package/packages/footer/src/index.ts +36 -3
- package/packages/footer/src/presets.ts +3 -4
- package/packages/footer/src/rendering/icons.ts +0 -20
- package/packages/footer/src/segments/compactor.ts +52 -142
- package/packages/footer/src/segments/status-ext.ts +0 -1
- package/packages/fusion/README.md +14 -4
- package/packages/fusion/package.json +4 -3
- package/packages/fusion/src/child-args.ts +1 -0
- package/packages/fusion/src/index.ts +161 -8
- package/packages/fusion/src/pi-settings.ts +57 -0
- package/packages/fusion/src/picker.ts +186 -51
- package/packages/fusion/src/preset.ts +56 -2
- package/packages/fusion/src/prompts.ts +1 -1
- package/packages/fusion/src/sidekick-runtime.ts +1 -420
- package/packages/fusion/src/tools.ts +107 -128
- package/packages/fusion/src/transcript.ts +27 -85
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/README.md +18 -30
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/input-shortcuts/src/burst.ts +88 -0
- package/packages/input-shortcuts/src/chord-overlay.ts +20 -70
- package/packages/input-shortcuts/src/cleared-input.ts +97 -0
- package/packages/input-shortcuts/src/index.ts +99 -193
- package/packages/input-shortcuts/src/last-response.ts +40 -0
- package/packages/input-shortcuts/src/registers.ts +8 -23
- package/packages/input-shortcuts/src/types.ts +4 -16
- package/packages/input-shortcuts/src/undo-redo.ts +59 -47
- package/packages/kanboard/README.md +51 -47
- package/packages/kanboard/index.ts +293 -55
- package/packages/kanboard/package.json +8 -8
- package/packages/kanboard/skills/kanboard/SKILL.md +61 -27
- package/packages/kanboard/src/badges.ts +93 -0
- package/packages/kanboard/src/bin.ts +4 -2
- package/packages/kanboard/src/commands.ts +218 -84
- package/packages/kanboard/src/debug.ts +20 -0
- package/packages/kanboard/src/guard.ts +267 -109
- package/packages/kanboard/src/monitor.ts +330 -0
- package/packages/kanboard/src/notice-buffer.ts +74 -0
- package/packages/kanboard/src/progress.ts +27 -0
- package/packages/kanboard/src/reminders.ts +234 -0
- package/packages/kanboard/src/settings.ts +69 -15
- package/packages/kanboard/src/shapes.ts +5 -0
- package/packages/long-horizon/index.ts +190 -13
- package/packages/long-horizon/package.json +3 -3
- package/packages/long-horizon/src/commands.ts +103 -7
- package/packages/long-horizon/src/compaction-brief.ts +35 -0
- package/packages/long-horizon/src/engine/continuation.ts +84 -15
- package/packages/long-horizon/src/engine/goal-state.ts +64 -13
- package/packages/long-horizon/src/engine/nudge-stash.ts +71 -0
- package/packages/long-horizon/src/engine/ralph.ts +3 -2
- package/packages/long-horizon/src/engine/runaway.ts +68 -11
- package/packages/long-horizon/src/engine/verifier.ts +19 -0
- package/packages/long-horizon/src/gate.ts +37 -12
- package/packages/long-horizon/src/judge/resolve.ts +1 -0
- package/packages/long-horizon/src/owner.ts +35 -2
- package/packages/long-horizon/src/progress.ts +87 -0
- package/packages/long-horizon/src/prompts/goal.ts +1 -1
- package/packages/long-horizon/src/runtime.ts +172 -18
- package/packages/long-horizon/src/settings.ts +54 -39
- package/packages/long-horizon/src/tools/goal.ts +6 -0
- package/packages/long-horizon/src/tools/graph.ts +1 -1
- package/packages/long-horizon/src/tools/swarm.ts +3 -3
- package/packages/mcp/package.json +2 -2
- package/packages/memory/commands.ts +7 -2
- package/packages/memory/index.ts +210 -32
- package/packages/memory/package.json +3 -3
- package/packages/memory/save-session.ts +264 -0
- package/packages/memory/settings.ts +17 -0
- package/packages/memory/tools.ts +253 -203
- package/packages/notify/package.json +2 -2
- package/packages/skill-registry/README.md +53 -16
- package/packages/skill-registry/index.ts +230 -0
- package/packages/skill-registry/package.json +19 -8
- package/packages/skill-registry/src/editor.ts +206 -0
- package/packages/skill-registry/src/judge.ts +289 -0
- package/packages/skill-registry/src/registry.ts +87 -0
- package/packages/skill-registry/src/settings.ts +259 -0
- package/packages/skill-registry/src/vault.ts +80 -0
- package/packages/subagents/README.md +39 -119
- package/packages/subagents/package.json +6 -13
- package/packages/subagents/skills/subagents/SKILL.md +18 -42
- package/packages/subagents/src/agents.ts +213 -0
- package/packages/subagents/src/cards.ts +182 -0
- package/packages/subagents/src/index.ts +609 -1084
- package/packages/subagents/src/manager.ts +429 -0
- package/packages/subagents/src/profiles.ts +130 -0
- package/packages/subagents/src/transcript.ts +154 -0
- package/packages/subagents/src/ui.ts +389 -0
- package/packages/unipi/bundled.js +39756 -45569
- package/packages/unipi/index.ts +55 -10
- package/packages/updater/package.json +2 -2
- package/packages/utility/README.md +76 -69
- package/packages/utility/package.json +2 -2
- package/packages/utility/skills/image/SKILL.md +37 -0
- package/packages/utility/skills/utility/SKILL.md +17 -60
- package/packages/utility/src/answer/extract.ts +128 -0
- package/packages/utility/src/answer/index.ts +213 -0
- package/packages/utility/src/answer/reply.ts +133 -0
- package/packages/utility/src/answer/web.ts +167 -0
- package/packages/utility/src/attach/index.ts +205 -0
- package/packages/utility/src/commands.ts +38 -142
- package/packages/utility/src/diagnostics/engine.ts +50 -5
- package/packages/utility/src/image/index.ts +33 -0
- package/packages/utility/src/image/models.ts +221 -0
- package/packages/{image/src → utility/src/image}/recognize.ts +2 -2
- package/packages/utility/src/image/settings.ts +178 -0
- package/packages/{image/src/image-source.ts → utility/src/image/source.ts} +1 -1
- package/packages/utility/src/image/tools.ts +168 -0
- package/packages/utility/src/image/transports.ts +186 -0
- package/packages/utility/src/index.ts +68 -389
- package/packages/utility/src/lifecycle/cleanup.ts +116 -311
- package/packages/utility/src/rename/gate.ts +102 -0
- package/packages/utility/src/rename/index.ts +191 -0
- package/packages/utility/src/rename/session.ts +170 -0
- package/packages/utility/src/render/delegated.ts +285 -0
- package/packages/utility/src/render/harness.ts +683 -0
- package/packages/utility/src/render/parse.ts +156 -0
- package/packages/utility/src/render/pi-settings.ts +38 -0
- package/packages/utility/src/render/reply-bg.ts +307 -0
- package/packages/utility/src/render/simple.ts +724 -0
- package/packages/utility/src/render/spacing.ts +235 -0
- package/packages/utility/src/render/styled.ts +251 -0
- package/packages/utility/src/render/tools.ts +353 -0
- package/packages/utility/src/settings.ts +166 -233
- package/packages/utility/src/types.ts +1 -222
- package/packages/watchdog/index.ts +17 -6
- package/packages/watchdog/package.json +3 -3
- package/packages/watchdog/src/config.ts +3 -2
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/README.md +3 -0
- package/packages/workflow/index.ts +0 -2
- package/packages/workflow/package.json +2 -2
- package/packages/workflow/src/permission/decide.ts +6 -4
- package/packages/workflow/src/permission/index.ts +17 -7
- package/packages/workflow/src/permission/settings.ts +39 -4
- package/packages/workflow/src/plan/index.ts +19 -4
- package/packages/background-tasks/extensions/delegate-child.ts +0 -1
- package/packages/background-tasks/src/context-parent-snapshot.ts +0 -142
- package/packages/background-tasks/src/context-token-budget.ts +0 -903
- package/packages/background-tasks/src/context-visible-conversation-v2.ts +0 -551
- package/packages/background-tasks/src/delegate/artifacts.ts +0 -487
- package/packages/background-tasks/src/delegate/budget.ts +0 -415
- package/packages/background-tasks/src/delegate/hook-contract-evidence.json +0 -18
- package/packages/background-tasks/src/delegate/hook-contract.ts +0 -154
- package/packages/background-tasks/src/delegate/launch.ts +0 -484
- package/packages/background-tasks/src/delegate/result-package.ts +0 -459
- package/packages/background-tasks/src/delegate/runner.ts +0 -430
- package/packages/background-tasks/src/delegate/seed.ts +0 -423
- package/packages/background-tasks/src/delegate/types.ts +0 -323
- package/packages/background-tasks/src/delegate-child-extension.ts +0 -978
- package/packages/background-tasks/src/delegate-extension.ts +0 -654
- package/packages/background-tasks/src/fixtures/delegate-context-incident.json +0 -17
- package/packages/compactor/skills/compactor-detail/SKILL.md +0 -116
- package/packages/compactor/skills/compactor-doctor/SKILL.md +0 -71
- package/packages/compactor/skills/compactor-stats/SKILL.md +0 -51
- package/packages/compactor/src/compaction/build-sections.ts +0 -76
- package/packages/compactor/src/compaction/extract/goals.ts +0 -36
- package/packages/compactor/src/compaction/extract/preferences.ts +0 -40
- package/packages/compactor/src/compaction/format.ts +0 -53
- package/packages/compactor/src/compaction/merge.ts +0 -143
- package/packages/compactor/src/config/presets.ts +0 -160
- package/packages/compactor/src/executor/executor.ts +0 -233
- package/packages/compactor/src/executor/runtime.ts +0 -115
- package/packages/compactor/src/security/evaluator.ts +0 -89
- package/packages/compactor/src/security/policy.ts +0 -96
- package/packages/compactor/src/security/scanner.ts +0 -65
- package/packages/compactor/src/session/auto-inject.ts +0 -60
- package/packages/compactor/src/session/db.ts +0 -325
- package/packages/compactor/src/session/extract.ts +0 -144
- package/packages/compactor/src/session/resume-inject.ts +0 -64
- package/packages/compactor/src/session/snapshot.ts +0 -327
- package/packages/compactor/src/tools/ctx-batch-execute.ts +0 -41
- package/packages/compactor/src/tools/ctx-execute-file.ts +0 -23
- package/packages/compactor/src/tools/ctx-execute.ts +0 -23
- package/packages/compactor/src/tools/ctx-stats.ts +0 -96
- package/packages/image/README.md +0 -99
- package/packages/image/package.json +0 -61
- package/packages/image/skills/image/SKILL.md +0 -76
- package/packages/image/src/generate.ts +0 -256
- package/packages/image/src/index.ts +0 -135
- package/packages/image/src/models.ts +0 -519
- package/packages/image/src/openai-images-api.ts +0 -282
- package/packages/image/src/register-providers.ts +0 -209
- package/packages/image/src/settings.ts +0 -173
- package/packages/image/src/tools.ts +0 -326
- package/packages/input-shortcuts/src/clipboard.ts +0 -119
- package/packages/kanboard/src/runner.ts +0 -817
- package/packages/skill-registry/skills/auto/SKILL.md +0 -465
- package/packages/skill-registry/skills/brainstorm/SKILL.md +0 -219
- package/packages/skill-registry/skills/chore-create/SKILL.md +0 -315
- package/packages/skill-registry/skills/chore-execute/SKILL.md +0 -314
- package/packages/skill-registry/skills/consolidate/SKILL.md +0 -142
- package/packages/skill-registry/skills/consultant/SKILL.md +0 -97
- package/packages/skill-registry/skills/debug/SKILL.md +0 -228
- package/packages/skill-registry/skills/document/SKILL.md +0 -120
- package/packages/skill-registry/skills/fix/SKILL.md +0 -212
- package/packages/skill-registry/skills/gather-context/SKILL.md +0 -145
- package/packages/skill-registry/skills/plan/SKILL.md +0 -231
- package/packages/skill-registry/skills/quick-fix/SKILL.md +0 -135
- package/packages/skill-registry/skills/quick-work/SKILL.md +0 -110
- package/packages/skill-registry/skills/research/SKILL.md +0 -251
- package/packages/skill-registry/skills/review-work/SKILL.md +0 -187
- package/packages/skill-registry/skills/scan-issues/SKILL.md +0 -195
- package/packages/skill-registry/skills/work/SKILL.md +0 -218
- package/packages/skill-registry/skills/worktree-create/SKILL.md +0 -69
- package/packages/skill-registry/skills/worktree-list/SKILL.md +0 -78
- package/packages/skill-registry/skills/worktree-merge/SKILL.md +0 -89
- package/packages/subagents/agents/delegate.md +0 -14
- package/packages/subagents/agents/oracle.md +0 -78
- package/packages/subagents/agents/researcher.md +0 -52
- package/packages/subagents/agents/reviewer.md +0 -80
- package/packages/subagents/agents/scout.md +0 -50
- package/packages/subagents/agents/worker.md +0 -59
- package/packages/subagents/prompts/council.md +0 -48
- package/packages/subagents/prompts/gather-context-and-clarify.md +0 -13
- package/packages/subagents/prompts/parallel-cleanup.md +0 -59
- package/packages/subagents/prompts/parallel-research.md +0 -50
- package/packages/subagents/prompts/parallel-review.md +0 -54
- package/packages/subagents/prompts/review-loop.md +0 -43
- package/packages/subagents/skills/council-mode/SKILL.md +0 -230
- package/packages/subagents/skills/subagents/references/constraints-and-recipes.md +0 -259
- package/packages/subagents/skills/subagents/references/execution-controls.md +0 -454
- package/packages/subagents/skills/subagents/references/management-authoring-rpc.md +0 -161
- package/packages/subagents/skills/subagents/references/multi-lane-orchestration.md +0 -39
- package/packages/subagents/skills/subagents/references/prompting-and-roles.md +0 -267
- package/packages/subagents/src/acceptance.ts +0 -331
- package/packages/subagents/src/agent-manager.ts +0 -405
- package/packages/subagents/src/agent-memory.ts +0 -300
- package/packages/subagents/src/agent-overrides.ts +0 -183
- package/packages/subagents/src/agent-runner.ts +0 -271
- package/packages/subagents/src/async-runner.ts +0 -422
- package/packages/subagents/src/authority-policy.ts +0 -47
- package/packages/subagents/src/budgets.ts +0 -285
- package/packages/subagents/src/child-safety.ts +0 -140
- package/packages/subagents/src/config.ts +0 -249
- package/packages/subagents/src/conversation-viewer.ts +0 -281
- package/packages/subagents/src/core-compat.ts +0 -94
- package/packages/subagents/src/custom-agents.ts +0 -343
- package/packages/subagents/src/file-system-retry.ts +0 -50
- package/packages/subagents/src/fleet-data.ts +0 -105
- package/packages/subagents/src/fleet-view.ts +0 -322
- package/packages/subagents/src/foreground-detach.ts +0 -59
- package/packages/subagents/src/fork-context.ts +0 -285
- package/packages/subagents/src/global.d.ts +0 -7
- package/packages/subagents/src/guide.ts +0 -129
- package/packages/subagents/src/mission-state.ts +0 -133
- package/packages/subagents/src/mission-store.ts +0 -436
- package/packages/subagents/src/output-limits.ts +0 -142
- package/packages/subagents/src/parity-types.ts +0 -315
- package/packages/subagents/src/pi-args.ts +0 -167
- package/packages/subagents/src/result-files.ts +0 -232
- package/packages/subagents/src/result-watcher.ts +0 -221
- package/packages/subagents/src/retained-children.ts +0 -176
- package/packages/subagents/src/run-fanout-budget.ts +0 -356
- package/packages/subagents/src/scheduled-runs.ts +0 -349
- package/packages/subagents/src/schemas.ts +0 -263
- package/packages/subagents/src/slash-commands.ts +0 -80
- package/packages/subagents/src/supervisor-channel.ts +0 -249
- package/packages/subagents/src/tool-handler.ts +0 -1144
- package/packages/subagents/src/types.ts +0 -238
- package/packages/subagents/src/widget.ts +0 -457
- package/packages/subagents/src/workflow-script.ts +0 -737
- package/packages/subagents/src/workflow-worker.ts +0 -384
- package/packages/subagents/src/worktree.ts +0 -614
- package/packages/utility/src/analytics/collector.ts +0 -293
- package/packages/utility/src/info-screen.ts +0 -80
- package/packages/utility/src/lifecycle/process.ts +0 -162
- package/packages/utility/src/prefix-cache.ts +0 -263
- package/packages/utility/src/skill-discovery.ts +0 -542
- package/packages/utility/src/tools/env.ts +0 -113
- package/packages/utility/src/tui/name-badge-state.ts +0 -315
- package/packages/utility/src/tui/name-badge.ts +0 -109
- /package/packages/{subagents/src → core/src/child-agent}/pi-spawn.ts +0 -0
|
@@ -1,379 +1,555 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Compaction hooks — every compaction (Pi's automatic ones, Pi's /compact,
|
|
3
|
+
* UniPi's commands, the percentage trigger) goes through here and is routed
|
|
4
|
+
* to one of two methods:
|
|
5
|
+
*
|
|
6
|
+
* vcc lossless zero-LLM summary (summarize.ts), rebuilt from full history
|
|
7
|
+
* llm Pi's model-written summary, with the active-work block appended
|
|
8
|
+
*
|
|
9
|
+
* The percentage trigger compacts at `turn_end` by returning a compaction
|
|
10
|
+
* draft — Pi applies it at the turn boundary without aborting the run, so
|
|
11
|
+
* goal / ralph / kanboard loops keep going. Pi 0.87 already continues after
|
|
12
|
+
* its own automatic compactions, so nothing here re-triggers the agent.
|
|
5
13
|
*/
|
|
6
14
|
|
|
7
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
SessionEntry,
|
|
11
|
-
SessionMessageEntry,
|
|
12
|
-
SessionBeforeCompactEvent,
|
|
13
|
-
SessionCompactEvent,
|
|
14
|
-
} from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { compileRanked } from "./summarize.js";
|
|
15
|
+
import type { ExtensionAPI, ExtensionContext, SessionBeforeCompactEvent, SessionCompactEvent } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { compact as piCompact, generateSummaryWithUsage, DEFAULT_COMPACTION_SETTINGS } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { collectCompactionContext, emitEvent, formatTokens, UNIPI_EVENTS } from "@pi-unipi/core";
|
|
16
18
|
import { loadConfig } from "../config/manager.js";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
resolveSmartKeepUserTurns,
|
|
20
|
-
applyTailBudget,
|
|
21
|
-
MAX_SMART_TAIL_TOKENS,
|
|
22
|
-
} from "./cut.js";
|
|
19
|
+
import { autoCompactionOf } from "../config/schema.js";
|
|
20
|
+
import { buildOwnCut, resolveSmartKeepUserTurns, applyTailBudget, MAX_SMART_TAIL_TOKENS } from "./cut.js";
|
|
23
21
|
import { parseCompactionInstructions } from "./compact-args.js";
|
|
22
|
+
import { calibrateCharsPerToken, estimateMessageContentChars, estimateTokensFromChars } from "./token-estimate.js";
|
|
23
|
+
import { collectSummarySource, ORIGIN_ENTRY_TYPE, originKey } from "./source.js";
|
|
24
|
+
import { autoBudgetTokens, buildLosslessSummary, type LosslessSummaryInput } from "./summarize.js";
|
|
25
|
+
import { CARD_TYPE, type CompactionCardData, type CompactionTrigger } from "../card.js";
|
|
24
26
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
createAutoCompactionState,
|
|
28
|
+
decideAutoCompaction,
|
|
29
|
+
markAutoCompactionComplete,
|
|
30
|
+
markAutoCompactionError,
|
|
31
|
+
type AutoCompactionState,
|
|
32
|
+
} from "./auto-trigger.js";
|
|
33
|
+
import type { BudgetCutKind, CompactionMethod, CompactionStats, CompactorConfig, FileOps, RuntimeCounters } from "../types.js";
|
|
34
|
+
|
|
35
|
+
export const COMPACTOR_ID = "@pi-unipi/compactor";
|
|
36
|
+
const SUMMARY_CHARS_PER_TOKEN = 4;
|
|
37
|
+
/** Kept-tail sizing against the model window: recut above LIMIT, down to SHARE. */
|
|
38
|
+
const TAIL_WINDOW_LIMIT = 0.4;
|
|
39
|
+
const TAIL_WINDOW_SHARE = 0.25;
|
|
40
|
+
|
|
41
|
+
/** Custom types from the pre-rework compactor, still present in old sessions. */
|
|
42
|
+
const LEGACY_HIDDEN_TYPES = new Set(["compactor-auto-continue", "unipi-compactor-resume"]);
|
|
33
43
|
|
|
34
44
|
let lastStats: CompactionStats | null = null;
|
|
35
|
-
|
|
45
|
+
/** Details of our last plan (sections, jev outcome) for the compaction card. */
|
|
46
|
+
let lastDetails: Record<string, unknown> | null = null;
|
|
47
|
+
let pendingMethod: CompactionMethod | null = null;
|
|
48
|
+
/** The command that started the next compaction, for the card. */
|
|
49
|
+
let pendingCommand: string | null = null;
|
|
50
|
+
let lastCommand: string | null = null;
|
|
51
|
+
/** Why Pi compacted (manual / threshold / overflow), captured before compaction. */
|
|
52
|
+
let lastReason: "manual" | "threshold" | "overflow" | undefined;
|
|
36
53
|
let pendingFollowUpPrompt: string | null = null;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
export const consumePendingFollowUpPrompt = (): string | null => {
|
|
40
|
-
const p = pendingFollowUpPrompt;
|
|
41
|
-
pendingFollowUpPrompt = null;
|
|
42
|
-
return p;
|
|
43
|
-
};
|
|
54
|
+
/** Set when a compaction was started by a UniPi command (it shows its own notice). */
|
|
55
|
+
let commandCompaction = false;
|
|
44
56
|
|
|
45
|
-
|
|
46
|
-
// Resume the agent after threshold/overflow compaction without polluting the
|
|
47
|
-
// LLM context: send a custom message marked with a dedicated customType
|
|
48
|
-
// (content:[], display:false, triggerTurn:true, deliverAs:'followUp') so Pi's
|
|
49
|
-
// queue/busy-state stays coherent; the on('context') filter registered below
|
|
50
|
-
// removes that message (by customType ONLY) from the LLM payload — the model
|
|
51
|
-
// simply continues from the compaction summary.
|
|
52
|
-
export const AUTO_CONTINUE_CUSTOM_TYPE = "compactor-auto-continue";
|
|
53
|
-
|
|
54
|
-
const clearPendingAutoContinue = () => {
|
|
55
|
-
if (pendingAutoContinueTimer) {
|
|
56
|
-
clearTimeout(pendingAutoContinueTimer);
|
|
57
|
-
pendingAutoContinueTimer = null;
|
|
58
|
-
}
|
|
59
|
-
};
|
|
57
|
+
export const getLastCompactionStats = () => lastStats;
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
details: undefined,
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
triggerTurn: true,
|
|
71
|
-
deliverAs: "followUp",
|
|
72
|
-
},
|
|
73
|
-
);
|
|
74
|
-
};
|
|
59
|
+
/** Route the next compaction to a method (used by /unipi:compact-vcc and -by-llm). */
|
|
60
|
+
export function setPendingCompaction(method: CompactionMethod | null, fromCommand = true, command?: string): void {
|
|
61
|
+
pendingMethod = method;
|
|
62
|
+
commandCompaction = method !== null && fromCommand;
|
|
63
|
+
pendingCommand = method !== null ? (command ?? null) : null;
|
|
64
|
+
}
|
|
75
65
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
66
|
+
/** Card data for a compaction we (or Pi) just made. */
|
|
67
|
+
export function buildCardData(opts: {
|
|
68
|
+
method: CompactionMethod;
|
|
69
|
+
trigger: CompactionTrigger;
|
|
70
|
+
tokensBefore: number;
|
|
71
|
+
stats: CompactionStats | null;
|
|
72
|
+
details: Record<string, unknown> | null;
|
|
73
|
+
summary?: string;
|
|
74
|
+
command?: string | null;
|
|
75
|
+
percent?: number;
|
|
76
|
+
threshold?: number;
|
|
77
|
+
}): CompactionCardData {
|
|
78
|
+
const { stats, details } = opts;
|
|
79
|
+
const summaryChars = typeof opts.summary === "string" ? opts.summary.length : 0;
|
|
80
|
+
return {
|
|
81
|
+
method: opts.method,
|
|
82
|
+
trigger: opts.trigger,
|
|
83
|
+
...(opts.command ? { command: opts.command } : {}),
|
|
84
|
+
tokensBefore: opts.tokensBefore,
|
|
85
|
+
...(stats?.tokensAfterEst != null ? { tokensAfter: stats.tokensAfterEst } : {}),
|
|
86
|
+
...(summaryChars ? { summaryTokens: Math.ceil(summaryChars / SUMMARY_CHARS_PER_TOKEN) } : {}),
|
|
87
|
+
...(stats ? { keptTurns: stats.keptUserTurns, totalTurns: stats.totalUserTurns, keptTokens: stats.keptTokensEst } : {}),
|
|
88
|
+
...(Array.isArray(details?.sections) ? { sections: details!.sections as string[] } : {}),
|
|
89
|
+
...(opts.percent != null ? { percent: opts.percent } : {}),
|
|
90
|
+
...(opts.threshold != null ? { threshold: opts.threshold } : {}),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
85
93
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
export const formatCompactionStats = (stats: CompactionStats): string => {
|
|
95
|
+
const after = stats.tokensAfterEst != null ? ` → ~${formatTokens(stats.tokensAfterEst)}` : "";
|
|
96
|
+
const before = stats.tokensBefore != null ? `${formatTokens(stats.tokensBefore)}${after} tok, ` : "";
|
|
97
|
+
if (stats.budgetCut) {
|
|
98
|
+
const reason = stats.budgetCut === "no_anchor" ? "no user turn to anchor on" : "oversized tail";
|
|
99
|
+
return `compactor: ${before}kept ~${formatTokens(stats.keptTokensEst)} tok tail (${reason}), summarized ${stats.summarized}.`;
|
|
100
|
+
}
|
|
101
|
+
const notes = [`summarized ${stats.summarized}`];
|
|
102
|
+
if (stats.smartKeepAdjusted) notes.push("smart-keep");
|
|
103
|
+
return `compactor: ${before}kept ${stats.keptUserTurns}/${stats.totalUserTurns} turns (${notes.join(", ")}).`;
|
|
89
104
|
};
|
|
90
105
|
|
|
91
106
|
const dbg = (debug: boolean, data: Record<string, unknown>) => {
|
|
92
107
|
if (!debug) return;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
writeFileSync("/tmp/compactor-debug.json", JSON.stringify(data, null, 2)),
|
|
97
|
-
).catch(() => {});
|
|
98
|
-
} catch {}
|
|
108
|
+
import("node:fs")
|
|
109
|
+
.then(({ writeFileSync }) => writeFileSync("/tmp/compactor-debug.json", JSON.stringify(data, null, 2)))
|
|
110
|
+
.catch(() => {});
|
|
99
111
|
};
|
|
100
112
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return content
|
|
105
|
-
.map((c: any) => {
|
|
106
|
-
if (c?.type === "text") return c.text ?? "";
|
|
107
|
-
if (c?.type === "toolCall") return `[toolCall:${c.name}]`;
|
|
108
|
-
if (c?.type === "thinking") return `[thinking]`;
|
|
109
|
-
if (c?.type === "image") return `[image:${c.mimeType}]`;
|
|
110
|
-
return `[${c?.type ?? "unknown"}]`;
|
|
111
|
-
})
|
|
112
|
-
.join("\n")
|
|
113
|
-
.slice(0, 300);
|
|
114
|
-
}
|
|
115
|
-
return "";
|
|
113
|
+
const readReason = (event: unknown): "manual" | "threshold" | "overflow" | undefined => {
|
|
114
|
+
const reason = (event as { reason?: unknown }).reason;
|
|
115
|
+
return reason === "manual" || reason === "threshold" || reason === "overflow" ? reason : undefined;
|
|
116
116
|
};
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const reason = stats.budgetCut === "no_anchor" ? "no user anchor" : "oversized tail";
|
|
122
|
-
return `compactor: kept ~${formatTokens(stats.keptTokensEst)} tok tail (mid-turn cut, ${reason}), summarized ${stats.summarized}.`;
|
|
123
|
-
}
|
|
124
|
-
const notes: string[] = [`summarized ${stats.summarized}`];
|
|
125
|
-
if (stats.smartKeepAdjusted) {
|
|
126
|
-
notes.push("smart-keep");
|
|
127
|
-
}
|
|
128
|
-
return `compactor: kept ${stats.keptUserTurns}/${stats.totalUserTurns} turns, ~${formatTokens(stats.keptTokensEst)} tok (${notes.join(", ")}).`;
|
|
118
|
+
const activeWorkText = (): string => {
|
|
119
|
+
const blocks = collectCompactionContext();
|
|
120
|
+
return blocks.length === 0 ? "" : `[Active Work]\n${blocks.map((b) => b.text).join("\n\n")}`;
|
|
129
121
|
};
|
|
130
122
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
123
|
+
// ── lossless plan ────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
export interface LosslessPlanInput {
|
|
126
|
+
branchEntries: readonly any[];
|
|
127
|
+
tokensBefore?: number;
|
|
128
|
+
previousSummary?: string;
|
|
129
|
+
fileOps?: FileOps;
|
|
130
|
+
keepUserTurns?: number | null;
|
|
131
|
+
keepExplicit?: boolean;
|
|
132
|
+
/** The model's context window; the kept tail is sized to fit it. */
|
|
133
|
+
contextWindow?: number;
|
|
134
|
+
config: CompactorConfig;
|
|
135
|
+
cwd?: string;
|
|
136
|
+
reason?: string;
|
|
145
137
|
}
|
|
146
138
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
139
|
+
export type LosslessPlan =
|
|
140
|
+
| { ok: true; summary: string; firstKeptEntryId: string; details: Record<string, unknown>; stats: CompactionStats; messageCount: number }
|
|
141
|
+
| { ok: false; reason: "no_live_messages" | "too_few_live_messages" };
|
|
142
|
+
|
|
143
|
+
/** Build a lossless compaction for the given branch (pure apart from active-work providers). */
|
|
144
|
+
export function planLosslessCompaction(input: LosslessPlanInput): LosslessPlan {
|
|
145
|
+
const branch = input.branchEntries as any[];
|
|
146
|
+
const { config } = input;
|
|
147
|
+
const explicit = input.keepExplicit === true;
|
|
148
|
+
|
|
149
|
+
// Calibrate chars/token from Pi's real token count vs the live message chars.
|
|
150
|
+
const calibrationCut = buildOwnCut(branch, 0);
|
|
151
|
+
const calibrationChars = calibrationCut.ok
|
|
152
|
+
? calibrationCut.messages.reduce((sum: number, m: any) => sum + estimateMessageContentChars(m.content), 0)
|
|
153
|
+
: 0;
|
|
154
|
+
const tokenEstimate = calibrateCharsPerToken(calibrationChars + (input.previousSummary?.length ?? 0), input.tokensBefore);
|
|
155
|
+
const cpt = tokenEstimate.charsPerToken;
|
|
156
|
+
|
|
157
|
+
const smartKeep = resolveSmartKeepUserTurns({
|
|
158
|
+
branchEntries: branch,
|
|
159
|
+
requestedKeepUserTurns: explicit ? (input.keepUserTurns ?? 1) : null,
|
|
160
|
+
explicit,
|
|
161
|
+
smartKeepTail: config.smartKeepTail,
|
|
162
|
+
charsPerToken: cpt,
|
|
163
|
+
});
|
|
164
|
+
let cut = buildOwnCut(branch, smartKeep.keepUserTurns);
|
|
165
|
+
if (cut.ok && !explicit) cut = applyTailBudget(branch, cut, { charsPerToken: cpt });
|
|
166
|
+
// Whatever keep asked for, the tail must leave room for the system prompt,
|
|
167
|
+
// the summary and the next turns: recut above 40% of the window (to 25%).
|
|
168
|
+
if (cut.ok && input.contextWindow && input.contextWindow > 0) {
|
|
169
|
+
cut = applyTailBudget(branch, cut, {
|
|
170
|
+
charsPerToken: cpt,
|
|
171
|
+
maxTokens: Math.floor(input.contextWindow * TAIL_WINDOW_SHARE),
|
|
172
|
+
oversizedFactor: TAIL_WINDOW_LIMIT / TAIL_WINDOW_SHARE,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (!cut.ok) return { ok: false, reason: cut.reason };
|
|
176
|
+
|
|
177
|
+
const keptIdx = cut.firstKeptEntryId ? branch.findIndex((e) => e.id === cut.firstKeptEntryId) : -1;
|
|
178
|
+
const endIdx = keptIdx >= 0 ? keptIdx : branch.length;
|
|
179
|
+
const keptChars = keptIdx >= 0
|
|
180
|
+
? branch.slice(keptIdx).reduce((sum, e) => sum + (e.type === "message" ? estimateMessageContentChars(e.message?.content) : 0), 0)
|
|
181
|
+
: 0;
|
|
182
|
+
const keptTokens = estimateTokensFromChars(keptChars, cpt);
|
|
183
|
+
|
|
184
|
+
const source = collectSummarySource(branch, endIdx);
|
|
185
|
+
const wanted = config.summaryBudgetTokens > 0 ? config.summaryBudgetTokens : autoBudgetTokens(source.blocks.length);
|
|
186
|
+
// Small-context models: the summary may not crowd out the work (≤8% of the window).
|
|
187
|
+
const budgetTokens = input.contextWindow && input.contextWindow > 0 ? Math.min(wanted, Math.max(800, Math.floor(input.contextWindow * 0.08))) : wanted;
|
|
188
|
+
const summaryInput: LosslessSummaryInput = {
|
|
189
|
+
source,
|
|
190
|
+
activeWork: config.sections.activeWork ? collectCompactionContext() : [],
|
|
191
|
+
// Summary text is plain prose: a fixed ratio, not the session's calibration
|
|
192
|
+
// (which images and code skew).
|
|
193
|
+
budgetChars: budgetTokens * SUMMARY_CHARS_PER_TOKEN,
|
|
194
|
+
cwd: input.cwd,
|
|
195
|
+
fileOps: input.fileOps,
|
|
196
|
+
sections: config.sections,
|
|
197
|
+
};
|
|
198
|
+
const summary = buildLosslessSummary(summaryInput);
|
|
199
|
+
|
|
200
|
+
const tokensAfter = keptTokens + estimateTokensFromChars(summary.text.length, SUMMARY_CHARS_PER_TOKEN);
|
|
201
|
+
const keptMessages = keptIdx >= 0 ? branch.slice(keptIdx).filter((e) => e.type === "message").length : 0;
|
|
202
|
+
const stats: CompactionStats = {
|
|
203
|
+
summarized: cut.messages.length,
|
|
204
|
+
kept: keptMessages,
|
|
205
|
+
totalMessages: cut.messages.length + keptMessages,
|
|
206
|
+
tokensBefore: input.tokensBefore,
|
|
207
|
+
tokensAfterEst: tokensAfter,
|
|
208
|
+
keptUserTurns: cut.keptUserTurns,
|
|
209
|
+
totalUserTurns: cut.totalUserTurns,
|
|
210
|
+
requestedKeepUserTurns: cut.requestedKeepUserTurns,
|
|
211
|
+
keepUserTurnsExplicit: explicit,
|
|
212
|
+
keepFallbackToCompactAll: cut.keepFallbackToCompactAll,
|
|
213
|
+
keptTokensEst: keptTokens,
|
|
214
|
+
smartKeepAdjusted: smartKeep.smartAdjusted,
|
|
215
|
+
smartFromKeep: smartKeep.fromKeep,
|
|
216
|
+
budgetCut: cut.budgetCut as BudgetCutKind | undefined,
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
ok: true,
|
|
220
|
+
summary: summary.text,
|
|
221
|
+
firstKeptEntryId: cut.firstKeptEntryId,
|
|
222
|
+
messageCount: cut.messages.length,
|
|
223
|
+
stats,
|
|
224
|
+
details: {
|
|
225
|
+
compactor: COMPACTOR_ID,
|
|
226
|
+
version: 2,
|
|
227
|
+
method: "vcc",
|
|
228
|
+
sections: summary.sections,
|
|
229
|
+
sourceMessageCount: cut.messages.length,
|
|
230
|
+
tokensAfter,
|
|
231
|
+
budgetTokens,
|
|
232
|
+
reason: input.reason,
|
|
233
|
+
budgetCut: cut.budgetCut,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
153
236
|
}
|
|
154
237
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
238
|
+
// ── model summary ────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
async function requestAuth(ctx: ExtensionContext) {
|
|
241
|
+
const model = ctx.model;
|
|
242
|
+
if (!model) throw new Error("no model selected");
|
|
243
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
244
|
+
if (!auth.ok) throw new Error(auth.error);
|
|
245
|
+
return {
|
|
246
|
+
model: auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model,
|
|
247
|
+
apiKey: auth.apiKey,
|
|
248
|
+
headers: auth.headers as Record<string, string> | undefined,
|
|
249
|
+
env: auth.env,
|
|
250
|
+
};
|
|
158
251
|
}
|
|
159
252
|
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
? raw.reason
|
|
164
|
-
: undefined;
|
|
165
|
-
return { reason, willRetry: raw.willRetry === true };
|
|
253
|
+
const joinInstructions = (...parts: Array<string | null | undefined>): string | undefined => {
|
|
254
|
+
const text = parts.map((p) => p?.trim()).filter(Boolean).join("\n\n");
|
|
255
|
+
return text || undefined;
|
|
166
256
|
};
|
|
167
257
|
|
|
168
|
-
|
|
258
|
+
const withActiveWork = (summary: string): string => {
|
|
259
|
+
const active = activeWorkText();
|
|
260
|
+
return active ? `${active}\n\n${summary}` : summary;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/** Pi's own summarizer over Pi's preparation, with active work prepended. */
|
|
264
|
+
async function llmFromPreparation(
|
|
169
265
|
pi: ExtensionAPI,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
266
|
+
event: SessionBeforeCompactEvent,
|
|
267
|
+
ctx: ExtensionContext,
|
|
268
|
+
instructions: string | undefined,
|
|
269
|
+
) {
|
|
270
|
+
const { model, apiKey, headers, env } = await requestAuth(ctx);
|
|
271
|
+
const result = await piCompact(
|
|
272
|
+
event.preparation,
|
|
273
|
+
model,
|
|
274
|
+
apiKey,
|
|
275
|
+
headers,
|
|
276
|
+
instructions,
|
|
277
|
+
event.signal,
|
|
278
|
+
pi.getThinkingLevel?.(),
|
|
279
|
+
undefined,
|
|
280
|
+
env,
|
|
281
|
+
);
|
|
282
|
+
return {
|
|
283
|
+
summary: withActiveWork(result.summary),
|
|
284
|
+
firstKeptEntryId: result.firstKeptEntryId,
|
|
285
|
+
tokensBefore: result.tokensBefore,
|
|
286
|
+
usage: result.usage,
|
|
287
|
+
details: { ...(result.details as object | undefined), compactor: COMPACTOR_ID, version: 2, method: "llm" },
|
|
288
|
+
};
|
|
289
|
+
}
|
|
185
290
|
|
|
186
|
-
|
|
187
|
-
clearPendingAutoContinue();
|
|
188
|
-
});
|
|
291
|
+
// ── registration ─────────────────────────────────────────
|
|
189
292
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const { isCompactor, keepUserTurns, keepUserTurnsExplicit, followUpPrompt } =
|
|
194
|
-
parseCompactionInstructions(customInstructions);
|
|
293
|
+
export interface CompactionHookDeps {
|
|
294
|
+
counters: RuntimeCounters;
|
|
295
|
+
}
|
|
195
296
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
297
|
+
export function registerCompactionHooks(pi: ExtensionAPI, deps: CompactionHookDeps): void {
|
|
298
|
+
let autoState: AutoCompactionState = createAutoCompactionState();
|
|
299
|
+
|
|
300
|
+
const afterCompaction = (stats: CompactionStats | null, method: CompactionMethod) => {
|
|
301
|
+
deps.counters.compactions++;
|
|
302
|
+
emitEvent(pi, UNIPI_EVENTS.COMPACTOR_COMPACTED, {
|
|
303
|
+
method,
|
|
304
|
+
summarized: stats?.summarized ?? 0,
|
|
305
|
+
kept: stats?.kept ?? 0,
|
|
306
|
+
tokensBefore: stats?.tokensBefore ?? 0,
|
|
307
|
+
tokensAfter: stats?.tokensAfterEst ?? 0,
|
|
308
|
+
tokensSaved: Math.max(0, (stats?.tokensBefore ?? 0) - (stats?.tokensAfterEst ?? 0)),
|
|
309
|
+
});
|
|
310
|
+
};
|
|
201
311
|
|
|
202
|
-
|
|
312
|
+
// Old sessions may still carry the removed auto-continue marker and the
|
|
313
|
+
// (huge) continuity snapshot; neither belongs in the model's context.
|
|
314
|
+
pi.on("context", (event) => {
|
|
315
|
+
const messages = (event as { messages: Array<{ role?: string; customType?: string }> }).messages;
|
|
316
|
+
const filtered = messages.filter((m) => m.role !== "custom" || !LEGACY_HIDDEN_TYPES.has(m.customType ?? ""));
|
|
317
|
+
if (filtered.length !== messages.length) return { messages: filtered } as any;
|
|
318
|
+
});
|
|
203
319
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const calibrationSummaryChars = typeof preparation.previousSummary === "string"
|
|
213
|
-
? preparation.previousSummary.length
|
|
214
|
-
: 0;
|
|
215
|
-
const tokenEstimate = calibrateCharsPerToken(
|
|
216
|
-
calibrationMessageChars + calibrationSummaryChars,
|
|
217
|
-
preparation.tokensBefore,
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
// Smart keep-tail: boost default keep when the tail is small.
|
|
221
|
-
// Explicit keep:N from the user is always respected (resolver no-ops).
|
|
222
|
-
const smartKeep = resolveSmartKeepUserTurns({
|
|
223
|
-
branchEntries: branchEntries as any[],
|
|
224
|
-
requestedKeepUserTurns: keepUserTurnsExplicit ? keepUserTurns : null,
|
|
225
|
-
explicit: keepUserTurnsExplicit,
|
|
226
|
-
smartKeepTail: config.smartKeepTail,
|
|
227
|
-
charsPerToken: tokenEstimate.charsPerToken,
|
|
228
|
-
});
|
|
229
|
-
let ownCut = buildOwnCut(branchEntries as any[], smartKeep.keepUserTurns);
|
|
230
|
-
// Default path only: rescue autonomous / oversized-tail sessions with a
|
|
231
|
-
// token-budget cut. Explicit keep:N is respected absolutely (no-op here).
|
|
232
|
-
if (ownCut.ok && !keepUserTurnsExplicit) {
|
|
233
|
-
ownCut = applyTailBudget(branchEntries as any[], ownCut, { charsPerToken: tokenEstimate.charsPerToken });
|
|
234
|
-
}
|
|
235
|
-
if (!ownCut.ok) {
|
|
236
|
-
if (!isCompactor && (readCompactionEventContext(event).reason === "overflow")) {
|
|
237
|
-
return; // let pi core retry
|
|
238
|
-
}
|
|
239
|
-
try {
|
|
240
|
-
ctx?.ui?.notify?.(REASON_MESSAGES[ownCut.reason] ?? ownCut.reason, "warning");
|
|
241
|
-
} catch {}
|
|
242
|
-
dbg(config.debug, { cancelled: true, reason: ownCut.reason });
|
|
243
|
-
return { cancel: true };
|
|
320
|
+
// Mark user messages that extensions send, so summaries can tell them from
|
|
321
|
+
// the user's own words (see source.ts).
|
|
322
|
+
pi.on("input", (event) => {
|
|
323
|
+
if (event.source !== "extension" || !event.text?.trim()) return;
|
|
324
|
+
try {
|
|
325
|
+
pi.appendEntry(ORIGIN_ENTRY_TYPE, { key: originKey(event.text) });
|
|
326
|
+
} catch {
|
|
327
|
+
// Never block input on bookkeeping.
|
|
244
328
|
}
|
|
329
|
+
});
|
|
245
330
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
// Count kept messages and estimate tokens
|
|
252
|
-
const keptIdx = (branchEntries as SessionEntry[]).findIndex((e: SessionEntry) => e.id === firstKeptEntryId);
|
|
253
|
-
const keptMessageEntries: SessionMessageEntry[] = keptIdx >= 0
|
|
254
|
-
? filterMessageEntries((branchEntries as SessionEntry[]).slice(keptIdx))
|
|
255
|
-
: [];
|
|
256
|
-
const keptChars = keptMessageEntries.reduce(
|
|
257
|
-
(sum: number, e: SessionMessageEntry) => sum + estimateMessageContentChars((e.message as any)?.content),
|
|
258
|
-
0,
|
|
259
|
-
);
|
|
260
|
-
|
|
261
|
-
const tokensBefore = preparation.tokensBefore;
|
|
262
|
-
lastStats = {
|
|
263
|
-
summarized: agentMessages.length,
|
|
264
|
-
kept: keptMessageEntries.length,
|
|
265
|
-
totalMessages: agentMessages.length + keptMessageEntries.length,
|
|
266
|
-
tokensBefore,
|
|
267
|
-
tokensAfterEst: estimateTokensFromChars(keptChars, tokenEstimate.charsPerToken),
|
|
268
|
-
keptUserTurns: ownCut.keptUserTurns,
|
|
269
|
-
totalUserTurns: ownCut.totalUserTurns,
|
|
270
|
-
requestedKeepUserTurns: ownCut.requestedKeepUserTurns,
|
|
271
|
-
keepUserTurnsExplicit,
|
|
272
|
-
keepFallbackToCompactAll: ownCut.keepFallbackToCompactAll,
|
|
273
|
-
keptTokensEst: estimateTokensFromChars(keptChars, tokenEstimate.charsPerToken),
|
|
274
|
-
smartKeepAdjusted: smartKeep.smartAdjusted,
|
|
275
|
-
smartFromKeep: smartKeep.fromKeep,
|
|
276
|
-
budgetCut: ownCut.budgetCut,
|
|
277
|
-
};
|
|
331
|
+
pi.on("session_start", () => {
|
|
332
|
+
autoState = createAutoCompactionState();
|
|
333
|
+
pendingMethod = null;
|
|
334
|
+
commandCompaction = false;
|
|
335
|
+
});
|
|
278
336
|
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
337
|
+
pi.on("session_before_compact", async (event: SessionBeforeCompactEvent, ctx) => {
|
|
338
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
339
|
+
const config = loadConfig(cwd);
|
|
340
|
+
const reason = readReason(event);
|
|
341
|
+
const parsed = parseCompactionInstructions(event.customInstructions);
|
|
342
|
+
const method: CompactionMethod = pendingMethod
|
|
343
|
+
?? (parsed.isCompactor
|
|
344
|
+
? "vcc"
|
|
345
|
+
: reason === "manual" && config.piCompact !== "follow"
|
|
346
|
+
? config.piCompact
|
|
347
|
+
: config.method);
|
|
348
|
+
const fromCommand = pendingMethod !== null && commandCompaction;
|
|
349
|
+
lastReason = reason;
|
|
350
|
+
lastCommand = fromCommand ? pendingCommand : null;
|
|
351
|
+
lastDetails = null;
|
|
352
|
+
pendingMethod = null;
|
|
353
|
+
pendingCommand = null;
|
|
354
|
+
pendingFollowUpPrompt = null;
|
|
355
|
+
|
|
356
|
+
if (method === "llm") {
|
|
357
|
+
const userInstructions = parsed.isCompactor ? undefined : event.customInstructions;
|
|
282
358
|
try {
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
} catch {
|
|
287
|
-
|
|
359
|
+
const compaction = await llmFromPreparation(pi, event, ctx, joinInstructions(userInstructions, config.llmInstructions));
|
|
360
|
+
lastStats = null;
|
|
361
|
+
return { compaction };
|
|
362
|
+
} catch (err) {
|
|
363
|
+
if (event.signal?.aborted) return { cancel: true };
|
|
364
|
+
// Fall back to Pi's own path (same summarizer, Pi's auth handling).
|
|
365
|
+
dbg(config.debug, { method: "llm", fallback: true, error: String(err) });
|
|
366
|
+
return;
|
|
288
367
|
}
|
|
289
368
|
}
|
|
290
369
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// with transcript length between a floor and a ceiling at
|
|
297
|
-
// RANKED_BRIEF_CHARS_PER_BLOCK per normalized block (pi-vcc parity).
|
|
298
|
-
const RANKED_BRIEF_BUDGET_TOKENS = 1100;
|
|
299
|
-
const RANKED_BRIEF_CEILING_TOKENS = 2000;
|
|
300
|
-
const RANKED_BRIEF_TOKENS_PER_BLOCK = 15;
|
|
301
|
-
const summary = compileRanked({
|
|
302
|
-
messages: messages as any,
|
|
303
|
-
previousSummary: preparation.previousSummary,
|
|
304
|
-
fileOps: preparation.fileOps
|
|
370
|
+
const planInput: LosslessPlanInput = {
|
|
371
|
+
branchEntries: event.branchEntries as any[],
|
|
372
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
373
|
+
previousSummary: event.preparation.previousSummary,
|
|
374
|
+
fileOps: event.preparation.fileOps
|
|
305
375
|
? {
|
|
306
|
-
readFiles: [...preparation.fileOps.read],
|
|
307
|
-
modifiedFiles: [...preparation.fileOps.written, ...preparation.fileOps.edited],
|
|
376
|
+
readFiles: [...event.preparation.fileOps.read],
|
|
377
|
+
modifiedFiles: [...event.preparation.fileOps.written, ...event.preparation.fileOps.edited],
|
|
308
378
|
}
|
|
309
379
|
: undefined,
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const details = {
|
|
318
|
-
compactor: "@pi-unipi/compactor",
|
|
319
|
-
version: 1,
|
|
320
|
-
sections: [...summary.matchAll(/^\[(.+?)\]/gm)].map((m) => m[1]),
|
|
321
|
-
sourceMessageCount: agentMessages.length,
|
|
322
|
-
previousSummaryUsed: Boolean(preparation.previousSummary),
|
|
323
|
-
reason: readCompactionEventContext(event).reason,
|
|
324
|
-
budgetCut: ownCut.budgetCut,
|
|
380
|
+
keepUserTurns: parsed.keepUserTurns,
|
|
381
|
+
keepExplicit: parsed.keepUserTurnsExplicit,
|
|
382
|
+
config,
|
|
383
|
+
cwd,
|
|
384
|
+
reason,
|
|
385
|
+
contextWindow: ctx?.model?.contextWindow,
|
|
325
386
|
};
|
|
387
|
+
const plan = planLosslessCompaction(planInput);
|
|
326
388
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
summaryPreview: summary.slice(0, 500),
|
|
339
|
-
sections: details.sections,
|
|
340
|
-
});
|
|
389
|
+
if (!plan.ok) {
|
|
390
|
+
// Overflow must still recover: let Pi's own summarizer handle it.
|
|
391
|
+
if (reason === "overflow") return;
|
|
392
|
+
if (reason === "manual" || fromCommand) {
|
|
393
|
+
try {
|
|
394
|
+
ctx?.ui?.notify?.(plan.reason === "no_live_messages" ? "compactor: nothing to compact" : "compactor: too few messages to compact", "warning");
|
|
395
|
+
} catch {}
|
|
396
|
+
}
|
|
397
|
+
dbg(config.debug, { cancelled: true, reason: plan.reason });
|
|
398
|
+
return { cancel: true };
|
|
399
|
+
}
|
|
341
400
|
|
|
401
|
+
lastStats = plan.stats;
|
|
402
|
+
lastDetails = plan.details;
|
|
403
|
+
if (!parsed.isCompactor && parsed.followUpPrompt && reason === "manual") pendingFollowUpPrompt = parsed.followUpPrompt;
|
|
404
|
+
dbg(config.debug, { method: "vcc", reason, stats: plan.stats, details: plan.details, summaryPreview: plan.summary.slice(0, 800) });
|
|
342
405
|
return {
|
|
343
406
|
compaction: {
|
|
344
|
-
summary,
|
|
345
|
-
details,
|
|
346
|
-
tokensBefore: preparation.tokensBefore,
|
|
347
|
-
firstKeptEntryId,
|
|
407
|
+
summary: plan.summary,
|
|
408
|
+
details: plan.details,
|
|
409
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
410
|
+
firstKeptEntryId: plan.firstKeptEntryId,
|
|
348
411
|
},
|
|
349
412
|
};
|
|
350
413
|
});
|
|
351
414
|
|
|
352
415
|
pi.on("session_compact", (event: SessionCompactEvent, ctx) => {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
416
|
+
const details = (event.compactionEntry as { details?: { compactor?: string; method?: CompactionMethod } } | undefined)?.details;
|
|
417
|
+
const ours = details?.compactor === COMPACTOR_ID;
|
|
418
|
+
const method: CompactionMethod = !ours ? "llm" : details?.method === "llm" ? "llm" : "vcc";
|
|
419
|
+
afterCompaction(ours ? lastStats : null, method);
|
|
420
|
+
|
|
421
|
+
const wasCommand = commandCompaction;
|
|
422
|
+
commandCompaction = false;
|
|
423
|
+
const followUp = pendingFollowUpPrompt;
|
|
424
|
+
pendingFollowUpPrompt = null;
|
|
425
|
+
// One line in the transcript (details on ctrl+o) instead of a toast.
|
|
426
|
+
// Commands always show it; automatic compactions follow Notifications.
|
|
427
|
+
if (wasCommand || loadConfig(ctx?.cwd ?? process.cwd()).notify) {
|
|
428
|
+
const entry = event.compactionEntry as { tokensBefore?: number; summary?: string } | undefined;
|
|
363
429
|
try {
|
|
364
|
-
|
|
430
|
+
pi.appendEntry(
|
|
431
|
+
CARD_TYPE,
|
|
432
|
+
buildCardData({
|
|
433
|
+
method,
|
|
434
|
+
trigger: lastReason === "threshold" || lastReason === "overflow" ? lastReason : "manual",
|
|
435
|
+
tokensBefore: Number(entry?.tokensBefore ?? lastStats?.tokensBefore ?? 0),
|
|
436
|
+
stats: ours ? lastStats : null,
|
|
437
|
+
details: ours ? lastDetails : null,
|
|
438
|
+
summary: entry?.summary,
|
|
439
|
+
command: wasCommand ? lastCommand : null,
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
365
442
|
} catch {}
|
|
366
|
-
}
|
|
367
|
-
if (
|
|
443
|
+
}
|
|
444
|
+
if (followUp) {
|
|
368
445
|
setTimeout(() => {
|
|
369
446
|
try {
|
|
370
|
-
|
|
447
|
+
pi.sendUserMessage(followUp, { deliverAs: "followUp" });
|
|
371
448
|
} catch {}
|
|
372
449
|
}, 0);
|
|
373
|
-
} else if (shouldContinueAfterAutoCompact) {
|
|
374
|
-
scheduleAutoContinue(pi);
|
|
375
450
|
}
|
|
376
451
|
});
|
|
452
|
+
|
|
453
|
+
// Percentage trigger: compact at the turn boundary (no abort, loops continue).
|
|
454
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
455
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
456
|
+
const config = loadConfig(cwd);
|
|
457
|
+
if (config.trigger !== "percent") return;
|
|
458
|
+
if (event.outcome === "aborted") return;
|
|
459
|
+
if (event.entries.some((draft) => draft.type === "compaction")) return;
|
|
460
|
+
|
|
461
|
+
const decision = decideAutoCompaction({
|
|
462
|
+
config: autoCompactionOf(config),
|
|
463
|
+
usage: ctx.getContextUsage?.(),
|
|
464
|
+
state: autoState,
|
|
465
|
+
nowMs: Date.now(),
|
|
466
|
+
});
|
|
467
|
+
autoState = decision.state;
|
|
468
|
+
if (!decision.shouldTrigger) return;
|
|
469
|
+
|
|
470
|
+
const branch = ctx.sessionManager.getBranch() as any[];
|
|
471
|
+
try {
|
|
472
|
+
const draft = config.method === "llm"
|
|
473
|
+
? await boundaryLlmDraft(pi, ctx, branch, config)
|
|
474
|
+
: await boundaryLosslessDraft(branch, config, cwd, decision.usage?.tokens, ctx.model?.contextWindow);
|
|
475
|
+
if (!draft) {
|
|
476
|
+
autoState = markAutoCompactionError(autoState, Date.now());
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
autoState = markAutoCompactionComplete(autoState);
|
|
480
|
+
afterCompaction(draft.stats, config.method);
|
|
481
|
+
// Boundary compactions skip session_compact: the card rides in the same draft, after the compaction.
|
|
482
|
+
const card = config.notify
|
|
483
|
+
? [{
|
|
484
|
+
type: "custom" as const,
|
|
485
|
+
customType: CARD_TYPE,
|
|
486
|
+
data: buildCardData({
|
|
487
|
+
method: config.method,
|
|
488
|
+
trigger: "percent",
|
|
489
|
+
tokensBefore: Number(draft.entry.usage?.tokensBefore ?? decision.usage?.tokens ?? draft.stats?.tokensBefore ?? 0),
|
|
490
|
+
stats: draft.stats,
|
|
491
|
+
details: (draft.entry.details as Record<string, unknown> | undefined) ?? null,
|
|
492
|
+
summary: draft.entry.summary,
|
|
493
|
+
percent: decision.usage?.percent,
|
|
494
|
+
threshold: decision.thresholdPercent,
|
|
495
|
+
}),
|
|
496
|
+
}]
|
|
497
|
+
: [];
|
|
498
|
+
return { entries: [draft.entry, ...card] };
|
|
499
|
+
} catch (err) {
|
|
500
|
+
autoState = markAutoCompactionError(autoState, Date.now());
|
|
501
|
+
if (config.notify) ctx.ui.notify(`Auto-compaction failed: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
type BoundaryDraft = {
|
|
508
|
+
entry: { type: "compaction"; summary: string; firstKeptEntryId: string | null; details?: unknown; usage?: any };
|
|
509
|
+
stats: CompactionStats | null;
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
async function boundaryLosslessDraft(branch: any[], config: CompactorConfig, cwd: string, tokensBefore?: number, contextWindow?: number): Promise<BoundaryDraft | null> {
|
|
513
|
+
const input: LosslessPlanInput = { branchEntries: branch, tokensBefore, config, cwd, reason: "percent", contextWindow };
|
|
514
|
+
const plan = planLosslessCompaction(input);
|
|
515
|
+
if (!plan.ok) return null;
|
|
516
|
+
lastStats = plan.stats;
|
|
517
|
+
return {
|
|
518
|
+
entry: { type: "compaction", summary: plan.summary, firstKeptEntryId: plan.firstKeptEntryId || null, details: plan.details },
|
|
519
|
+
stats: plan.stats,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function boundaryLlmDraft(pi: ExtensionAPI, ctx: ExtensionContext, branch: any[], config: CompactorConfig): Promise<BoundaryDraft | null> {
|
|
524
|
+
const cut = buildOwnCut(branch, 1);
|
|
525
|
+
const tail = cut.ok ? applyTailBudget(branch, cut) : cut;
|
|
526
|
+
if (!tail.ok || tail.messages.length === 0) return null;
|
|
527
|
+
const previous = [...branch].reverse().find((e) => e.type === "compaction")?.summary as string | undefined;
|
|
528
|
+
const { model, apiKey, headers, env } = await requestAuth(ctx);
|
|
529
|
+
const result = await generateSummaryWithUsage(
|
|
530
|
+
tail.messages as any,
|
|
531
|
+
model,
|
|
532
|
+
DEFAULT_COMPACTION_SETTINGS.reserveTokens,
|
|
533
|
+
apiKey,
|
|
534
|
+
headers,
|
|
535
|
+
undefined,
|
|
536
|
+
joinInstructions(config.llmInstructions),
|
|
537
|
+
previous,
|
|
538
|
+
pi.getThinkingLevel?.(),
|
|
539
|
+
undefined,
|
|
540
|
+
env,
|
|
541
|
+
);
|
|
542
|
+
lastStats = null;
|
|
543
|
+
return {
|
|
544
|
+
entry: {
|
|
545
|
+
type: "compaction",
|
|
546
|
+
summary: withActiveWork(result.text),
|
|
547
|
+
firstKeptEntryId: tail.firstKeptEntryId || null,
|
|
548
|
+
details: { compactor: COMPACTOR_ID, version: 2, method: "llm", reason: "percent" },
|
|
549
|
+
usage: result.usage,
|
|
550
|
+
},
|
|
551
|
+
stats: null,
|
|
552
|
+
};
|
|
377
553
|
}
|
|
378
554
|
|
|
379
555
|
export { MAX_SMART_TAIL_TOKENS };
|