compressed-context 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/package.json +64 -64
  2. package/src/compression/active-summary-cache.ts +50 -50
  3. package/src/compression/adopt.ts +57 -57
  4. package/src/compression/auto.ts +372 -372
  5. package/src/compression/babel.ts +85 -85
  6. package/src/compression/block.ts +25 -25
  7. package/src/compression/cache-history.ts +75 -75
  8. package/src/compression/compaction.ts +189 -189
  9. package/src/compression/decompress-tool.ts +74 -74
  10. package/src/compression/decompress.ts +292 -292
  11. package/src/compression/deleted-sessions.ts +24 -24
  12. package/src/compression/distill.ts +220 -220
  13. package/src/compression/events.ts +275 -275
  14. package/src/compression/markdown.ts +53 -53
  15. package/src/compression/normalize.ts +114 -114
  16. package/src/compression/render.ts +70 -70
  17. package/src/compression/schema.ts +84 -84
  18. package/src/compression/select.ts +123 -123
  19. package/src/compression/session-message.ts +112 -112
  20. package/src/compression/settings-rpc.ts +111 -111
  21. package/src/compression/settings.ts +175 -175
  22. package/src/compression/state.ts +248 -248
  23. package/src/compression/summarize.ts +595 -595
  24. package/src/compression/summary.ts +73 -73
  25. package/src/compression/tool.ts +142 -142
  26. package/src/compression/transcript-store.ts +258 -258
  27. package/src/compression/transcript.ts +63 -63
  28. package/src/compression/validate.ts +81 -81
  29. package/src/i18n/en.ts +93 -93
  30. package/src/i18n/index.ts +28 -28
  31. package/src/i18n/types.ts +89 -89
  32. package/src/i18n/zh.ts +93 -93
  33. package/src/index.ts +108 -108
  34. package/src/shared/blocks.ts +17 -17
  35. package/src/shared/cache.ts +199 -199
  36. package/src/shared/format.ts +23 -23
  37. package/src/shared/media.ts +184 -184
  38. package/src/shared/parse.ts +25 -25
  39. package/src/shared/parts.ts +72 -72
  40. package/src/shared/rpc.ts +145 -145
  41. package/src/shared/scan.ts +32 -32
  42. package/src/shared/settings.ts +471 -471
  43. package/src/shared/stats.ts +142 -142
  44. package/src/shared/tokens.ts +259 -259
  45. package/src/tui/chart/cache-chart.ts +237 -237
  46. package/src/tui/chart/color.ts +58 -58
  47. package/src/tui/index.tsx +111 -111
  48. package/src/tui/panel/cache.tsx +203 -203
  49. package/src/tui/panel/data.ts +105 -105
  50. package/src/tui/panel/index.tsx +176 -176
  51. package/src/tui/panel/summaries.tsx +192 -192
  52. package/src/tui/settings/dialogs.tsx +215 -215
  53. package/src/tui/settings/options.ts +238 -238
  54. package/src/tui/settings.tsx +418 -418
  55. package/src/tui/types.ts +4 -4
@@ -1,220 +1,220 @@
1
- import type { Plugin } from "@opencode/plugin"
2
-
3
- import type { SummaryFormat, SummaryLanguage } from "../shared/settings.ts"
4
- import { countText } from "../shared/tokens.ts"
5
- import { activeBlocks, renderAll, renderBlock } from "./render.ts"
6
- import { nextBlockId, type Block, type CompactionPhase, type State, type Tier } from "./state.ts"
7
- import { summaryChars, summaryGoal, summaryTitle } from "./summary.ts"
8
- import {
9
- buildMergeInstruction,
10
- sessionGenerator,
11
- summarizeChoiceInstruction,
12
- type SummaryChoiceCandidate,
13
- } from "./summarize.ts"
14
- import { minSummaryChars, validateSummary } from "./validate.ts"
15
-
16
- /** 一个合法的相邻合并候选。 */
17
- export interface MergeCandidate {
18
- readonly left: Block
19
- readonly right: Block
20
- readonly tier: Tier
21
- }
22
-
23
- export interface DistillOnceInput {
24
- readonly ctx: Plugin.Context
25
- readonly sessionID: string
26
- readonly state: State
27
- /** 上一层还没提交的块;T2/T3 钩子里可能来自 pending。 */
28
- readonly pending: Block | undefined
29
- readonly budgetTokens: number
30
- readonly summaryBlockChars?: number
31
- readonly summaryTotalChars?: number
32
- readonly summaryLanguage?: SummaryLanguage
33
- readonly summaryFormat?: SummaryFormat
34
- readonly phase: Exclude<CompactionPhase, undefined>
35
- readonly showTimeline?: boolean
36
- readonly showBlockTokens?: boolean
37
- }
38
-
39
- export interface DistillOnceResult {
40
- readonly blocks: readonly Block[]
41
- readonly pending: Block | undefined
42
- readonly summary: string
43
- readonly merged: boolean
44
- readonly nextPhase?: CompactionPhase
45
- readonly error?: string
46
- }
47
-
48
- /** 输入:active 链;输出:所有保持 tier 单调不增的相邻合并候选。 */
49
- export function mergeCandidates(blocks: readonly Block[]): readonly MergeCandidate[] {
50
- const candidates: MergeCandidate[] = []
51
- for (let index = 0; index < blocks.length - 1; index += 1) {
52
- const left = blocks[index]
53
- const right = blocks[index + 1]
54
- if (left === undefined || right === undefined) continue
55
- const tier = Math.min(3, Math.max(left.tier, right.tier) + 1) as Tier
56
- const previous = blocks[index - 1]
57
- const next = blocks[index + 2]
58
- if (previous !== undefined && previous.tier < tier) continue
59
- if (next !== undefined && tier < next.tier) continue
60
- candidates.push({ left, right, tier })
61
- }
62
- return candidates
63
- }
64
-
65
- /** 输入:T1 后的摘要链;输出:是否要自动排 T2。 */
66
- export function planT1Phase(
67
- summary: string,
68
- active: readonly Block[],
69
- budgetTokens: number,
70
- totalChars: number = Number.POSITIVE_INFINITY,
71
- ): CompactionPhase | undefined {
72
- return (countText(summary) > budgetTokens || summary.length > totalChars) &&
73
- mergeCandidates(active).length > 0
74
- ? "t2"
75
- : undefined
76
- }
77
-
78
- /**
79
- * 输入:一次蒸馏结果 + active 链 + 预算。
80
- * 输出:是否已经无法继续压缩(仍超预算,且没有任何合法合并候选)。
81
- * 预期:与层级无关——全 T3 只要还有两块可合并,或链本身没超预算,都不算终止。
82
- */
83
- export function isTerminal(
84
- result: DistillOnceResult,
85
- active: readonly Block[],
86
- budgetTokens: number,
87
- totalChars: number,
88
- ): boolean {
89
- if (result.merged || result.error !== undefined) return false
90
- if (countText(result.summary) <= budgetTokens && result.summary.length <= totalChars) return false
91
- return mergeCandidates(active).length === 0
92
- }
93
-
94
- /** 输入:块;输出:候选列表里的简短描述。 */
95
- function describe(block: Block): string {
96
- const goal = summaryGoal(block.summary).replace(/\s+/g, " ").slice(0, 160)
97
- return `${block.id} T${block.tier.toString()} "${summaryTitle(block.summary)}" — ${goal}`
98
- }
99
-
100
- /** 输入:候选;输出:这两块的完整 transcript(只用于本地校验,不发给模型)。 */
101
- function pairTranscript(candidate: MergeCandidate): string {
102
- return `${renderBlock(candidate.left)}\n\n${renderBlock(candidate.right)}`
103
- }
104
-
105
- /**
106
- * 输入:当前状态 + 上一层 pending + 预算 + 本次是 T2 还是 T3。
107
- * 输出:只合并一对后的 blocks / pending / summary。
108
- * 预期:每层只合一次;模型在会话前缀后只看到追加的候选指令。
109
- */
110
- export async function distillOnce(input: DistillOnceInput): Promise<DistillOnceResult> {
111
- const renderOptions = {
112
- showTimeline: input.showTimeline === true,
113
- showBlockTokens: input.showBlockTokens === true,
114
- }
115
- const active = activeBlocks(input.state.blocks, input.pending)
116
- const currentSummary = renderAll(input.state.blocks, input.pending, renderOptions)
117
- const totalChars = input.summaryTotalChars ?? Number.POSITIVE_INFINITY
118
- if (
119
- active.length < 2 ||
120
- (countText(currentSummary) <= input.budgetTokens && currentSummary.length <= totalChars)
121
- ) {
122
- return { blocks: input.state.blocks, pending: input.pending, summary: currentSummary, merged: false }
123
- }
124
-
125
- const candidates = mergeCandidates(active)
126
- if (candidates.length === 0) {
127
- return { blocks: input.state.blocks, pending: input.pending, summary: currentSummary, merged: false }
128
- }
129
-
130
- const choices: SummaryChoiceCandidate[] = candidates.map((candidate, index) => ({
131
- index: index + 1,
132
- description: `${describe(candidate.left)} + ${describe(candidate.right)} → T${candidate.tier.toString()}`,
133
- sourceChars: pairTranscript(candidate).length,
134
- tier: candidate.tier,
135
- }))
136
-
137
- const generator = sessionGenerator(input.ctx.session, input.sessionID)
138
- let failure = "distill summary failed"
139
- const promptOptions = {
140
- ...(input.summaryBlockChars === undefined ? {} : { maxChars: input.summaryBlockChars }),
141
- ...(input.summaryTotalChars === undefined ? {} : { totalChars: input.summaryTotalChars }),
142
- ...(input.summaryLanguage === undefined ? {} : { language: input.summaryLanguage }),
143
- format: input.summaryFormat ?? "json",
144
- }
145
- const picked = await summarizeChoiceInstruction({
146
- prompt: buildMergeInstruction(choices, promptOptions),
147
- candidates: choices,
148
- generate: generator,
149
- ...(input.summaryBlockChars === undefined ? {} : { maxChars: input.summaryBlockChars }),
150
- format: input.summaryFormat ?? "json",
151
- onFailure: (reason) => {
152
- failure = `distill summary failed: ${reason}`
153
- },
154
- })
155
- if (picked === undefined) {
156
- return {
157
- blocks: input.state.blocks,
158
- pending: input.pending,
159
- summary: currentSummary,
160
- merged: false,
161
- error: failure,
162
- }
163
- }
164
-
165
- const chosen = candidates[picked.candidate.index - 1]
166
- if (chosen === undefined) {
167
- return {
168
- blocks: input.state.blocks,
169
- pending: input.pending,
170
- summary: currentSummary,
171
- merged: false,
172
- error: "model picked an unknown candidate",
173
- }
174
- }
175
-
176
- const validation = validateSummary(
177
- picked.summary,
178
- pairTranscript(chosen).length,
179
- minSummaryChars(chosen.tier),
180
- input.summaryBlockChars,
181
- )
182
- if (!validation.ok) {
183
- return {
184
- blocks: input.state.blocks,
185
- pending: input.pending,
186
- summary: currentSummary,
187
- merged: false,
188
- error: validation.message,
189
- }
190
- }
191
-
192
- // 上一层 pending 先转成已落盘块,新合并块保持 pending,交给 compaction 事件转正。
193
- const working = input.pending === undefined ? [...input.state.blocks] : [...input.state.blocks, input.pending]
194
- const merged: Block = {
195
- id: nextBlockId(working),
196
- tier: chosen.tier,
197
- summary: picked.summary,
198
- fromId: chosen.left.fromId,
199
- toId: chosen.right.toId,
200
- tokensBefore: chosen.left.tokensAfter + chosen.right.tokensAfter,
201
- tokensAfter: Math.ceil(summaryChars(picked.summary) / 4),
202
- at: Math.max(chosen.left.at, chosen.right.at),
203
- order: chosen.left.order,
204
- sourceBlockIds: [chosen.left.id, chosen.right.id],
205
- }
206
- const summary = renderAll(working, merged, renderOptions)
207
- const stillOver =
208
- countText(summary) > input.budgetTokens || summary.length > totalChars
209
- // 只要还超预算且还有合法候选就继续合并;T3+T3 产出仍是 T3,直到没有候选或压进预算。
210
- const nextPhase: CompactionPhase | undefined =
211
- stillOver && mergeCandidates(activeBlocks(working, merged)).length > 0 ? "t3" : undefined
212
-
213
- return {
214
- blocks: working,
215
- pending: merged,
216
- summary,
217
- merged: true,
218
- ...(nextPhase === undefined ? {} : { nextPhase }),
219
- }
220
- }
1
+ import type { Plugin } from "@opencode/plugin"
2
+
3
+ import type { SummaryFormat, SummaryLanguage } from "../shared/settings.ts"
4
+ import { countText } from "../shared/tokens.ts"
5
+ import { activeBlocks, renderAll, renderBlock } from "./render.ts"
6
+ import { nextBlockId, type Block, type CompactionPhase, type State, type Tier } from "./state.ts"
7
+ import { summaryChars, summaryGoal, summaryTitle } from "./summary.ts"
8
+ import {
9
+ buildMergeInstruction,
10
+ sessionGenerator,
11
+ summarizeChoiceInstruction,
12
+ type SummaryChoiceCandidate,
13
+ } from "./summarize.ts"
14
+ import { minSummaryChars, validateSummary } from "./validate.ts"
15
+
16
+ /** 一个合法的相邻合并候选。 */
17
+ export interface MergeCandidate {
18
+ readonly left: Block
19
+ readonly right: Block
20
+ readonly tier: Tier
21
+ }
22
+
23
+ export interface DistillOnceInput {
24
+ readonly ctx: Plugin.Context
25
+ readonly sessionID: string
26
+ readonly state: State
27
+ /** 上一层还没提交的块;T2/T3 钩子里可能来自 pending。 */
28
+ readonly pending: Block | undefined
29
+ readonly budgetTokens: number
30
+ readonly summaryBlockChars?: number
31
+ readonly summaryTotalChars?: number
32
+ readonly summaryLanguage?: SummaryLanguage
33
+ readonly summaryFormat?: SummaryFormat
34
+ readonly phase: Exclude<CompactionPhase, undefined>
35
+ readonly showTimeline?: boolean
36
+ readonly showBlockTokens?: boolean
37
+ }
38
+
39
+ export interface DistillOnceResult {
40
+ readonly blocks: readonly Block[]
41
+ readonly pending: Block | undefined
42
+ readonly summary: string
43
+ readonly merged: boolean
44
+ readonly nextPhase?: CompactionPhase
45
+ readonly error?: string
46
+ }
47
+
48
+ /** 输入:active 链;输出:所有保持 tier 单调不增的相邻合并候选。 */
49
+ export function mergeCandidates(blocks: readonly Block[]): readonly MergeCandidate[] {
50
+ const candidates: MergeCandidate[] = []
51
+ for (let index = 0; index < blocks.length - 1; index += 1) {
52
+ const left = blocks[index]
53
+ const right = blocks[index + 1]
54
+ if (left === undefined || right === undefined) continue
55
+ const tier = Math.min(3, Math.max(left.tier, right.tier) + 1) as Tier
56
+ const previous = blocks[index - 1]
57
+ const next = blocks[index + 2]
58
+ if (previous !== undefined && previous.tier < tier) continue
59
+ if (next !== undefined && tier < next.tier) continue
60
+ candidates.push({ left, right, tier })
61
+ }
62
+ return candidates
63
+ }
64
+
65
+ /** 输入:T1 后的摘要链;输出:是否要自动排 T2。 */
66
+ export function planT1Phase(
67
+ summary: string,
68
+ active: readonly Block[],
69
+ budgetTokens: number,
70
+ totalChars: number = Number.POSITIVE_INFINITY,
71
+ ): CompactionPhase | undefined {
72
+ return (countText(summary) > budgetTokens || summary.length > totalChars) &&
73
+ mergeCandidates(active).length > 0
74
+ ? "t2"
75
+ : undefined
76
+ }
77
+
78
+ /**
79
+ * 输入:一次蒸馏结果 + active 链 + 预算。
80
+ * 输出:是否已经无法继续压缩(仍超预算,且没有任何合法合并候选)。
81
+ * 预期:与层级无关——全 T3 只要还有两块可合并,或链本身没超预算,都不算终止。
82
+ */
83
+ export function isTerminal(
84
+ result: DistillOnceResult,
85
+ active: readonly Block[],
86
+ budgetTokens: number,
87
+ totalChars: number,
88
+ ): boolean {
89
+ if (result.merged || result.error !== undefined) return false
90
+ if (countText(result.summary) <= budgetTokens && result.summary.length <= totalChars) return false
91
+ return mergeCandidates(active).length === 0
92
+ }
93
+
94
+ /** 输入:块;输出:候选列表里的简短描述。 */
95
+ function describe(block: Block): string {
96
+ const goal = summaryGoal(block.summary).replace(/\s+/g, " ").slice(0, 160)
97
+ return `${block.id} T${block.tier.toString()} "${summaryTitle(block.summary)}" — ${goal}`
98
+ }
99
+
100
+ /** 输入:候选;输出:这两块的完整 transcript(只用于本地校验,不发给模型)。 */
101
+ function pairTranscript(candidate: MergeCandidate): string {
102
+ return `${renderBlock(candidate.left)}\n\n${renderBlock(candidate.right)}`
103
+ }
104
+
105
+ /**
106
+ * 输入:当前状态 + 上一层 pending + 预算 + 本次是 T2 还是 T3。
107
+ * 输出:只合并一对后的 blocks / pending / summary。
108
+ * 预期:每层只合一次;模型在会话前缀后只看到追加的候选指令。
109
+ */
110
+ export async function distillOnce(input: DistillOnceInput): Promise<DistillOnceResult> {
111
+ const renderOptions = {
112
+ showTimeline: input.showTimeline === true,
113
+ showBlockTokens: input.showBlockTokens === true,
114
+ }
115
+ const active = activeBlocks(input.state.blocks, input.pending)
116
+ const currentSummary = renderAll(input.state.blocks, input.pending, renderOptions)
117
+ const totalChars = input.summaryTotalChars ?? Number.POSITIVE_INFINITY
118
+ if (
119
+ active.length < 2 ||
120
+ (countText(currentSummary) <= input.budgetTokens && currentSummary.length <= totalChars)
121
+ ) {
122
+ return { blocks: input.state.blocks, pending: input.pending, summary: currentSummary, merged: false }
123
+ }
124
+
125
+ const candidates = mergeCandidates(active)
126
+ if (candidates.length === 0) {
127
+ return { blocks: input.state.blocks, pending: input.pending, summary: currentSummary, merged: false }
128
+ }
129
+
130
+ const choices: SummaryChoiceCandidate[] = candidates.map((candidate, index) => ({
131
+ index: index + 1,
132
+ description: `${describe(candidate.left)} + ${describe(candidate.right)} → T${candidate.tier.toString()}`,
133
+ sourceChars: pairTranscript(candidate).length,
134
+ tier: candidate.tier,
135
+ }))
136
+
137
+ const generator = sessionGenerator(input.ctx.session, input.sessionID)
138
+ let failure = "distill summary failed"
139
+ const promptOptions = {
140
+ ...(input.summaryBlockChars === undefined ? {} : { maxChars: input.summaryBlockChars }),
141
+ ...(input.summaryTotalChars === undefined ? {} : { totalChars: input.summaryTotalChars }),
142
+ ...(input.summaryLanguage === undefined ? {} : { language: input.summaryLanguage }),
143
+ format: input.summaryFormat ?? "json",
144
+ }
145
+ const picked = await summarizeChoiceInstruction({
146
+ prompt: buildMergeInstruction(choices, promptOptions),
147
+ candidates: choices,
148
+ generate: generator,
149
+ ...(input.summaryBlockChars === undefined ? {} : { maxChars: input.summaryBlockChars }),
150
+ format: input.summaryFormat ?? "json",
151
+ onFailure: (reason) => {
152
+ failure = `distill summary failed: ${reason}`
153
+ },
154
+ })
155
+ if (picked === undefined) {
156
+ return {
157
+ blocks: input.state.blocks,
158
+ pending: input.pending,
159
+ summary: currentSummary,
160
+ merged: false,
161
+ error: failure,
162
+ }
163
+ }
164
+
165
+ const chosen = candidates[picked.candidate.index - 1]
166
+ if (chosen === undefined) {
167
+ return {
168
+ blocks: input.state.blocks,
169
+ pending: input.pending,
170
+ summary: currentSummary,
171
+ merged: false,
172
+ error: "model picked an unknown candidate",
173
+ }
174
+ }
175
+
176
+ const validation = validateSummary(
177
+ picked.summary,
178
+ pairTranscript(chosen).length,
179
+ minSummaryChars(chosen.tier),
180
+ input.summaryBlockChars,
181
+ )
182
+ if (!validation.ok) {
183
+ return {
184
+ blocks: input.state.blocks,
185
+ pending: input.pending,
186
+ summary: currentSummary,
187
+ merged: false,
188
+ error: validation.message,
189
+ }
190
+ }
191
+
192
+ // 上一层 pending 先转成已落盘块,新合并块保持 pending,交给 compaction 事件转正。
193
+ const working = input.pending === undefined ? [...input.state.blocks] : [...input.state.blocks, input.pending]
194
+ const merged: Block = {
195
+ id: nextBlockId(working),
196
+ tier: chosen.tier,
197
+ summary: picked.summary,
198
+ fromId: chosen.left.fromId,
199
+ toId: chosen.right.toId,
200
+ tokensBefore: chosen.left.tokensAfter + chosen.right.tokensAfter,
201
+ tokensAfter: Math.ceil(summaryChars(picked.summary) / 4),
202
+ at: Math.max(chosen.left.at, chosen.right.at),
203
+ order: chosen.left.order,
204
+ sourceBlockIds: [chosen.left.id, chosen.right.id],
205
+ }
206
+ const summary = renderAll(working, merged, renderOptions)
207
+ const stillOver =
208
+ countText(summary) > input.budgetTokens || summary.length > totalChars
209
+ // 只要还超预算且还有合法候选就继续合并;T3+T3 产出仍是 T3,直到没有候选或压进预算。
210
+ const nextPhase: CompactionPhase | undefined =
211
+ stillOver && mergeCandidates(activeBlocks(working, merged)).length > 0 ? "t3" : undefined
212
+
213
+ return {
214
+ blocks: working,
215
+ pending: merged,
216
+ summary,
217
+ merged: true,
218
+ ...(nextPhase === undefined ? {} : { nextPhase }),
219
+ }
220
+ }