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.
- package/package.json +64 -64
- package/src/compression/active-summary-cache.ts +50 -50
- package/src/compression/adopt.ts +57 -57
- package/src/compression/auto.ts +372 -372
- package/src/compression/babel.ts +85 -85
- package/src/compression/block.ts +25 -25
- package/src/compression/cache-history.ts +75 -75
- package/src/compression/compaction.ts +189 -189
- package/src/compression/decompress-tool.ts +74 -74
- package/src/compression/decompress.ts +292 -292
- package/src/compression/deleted-sessions.ts +24 -24
- package/src/compression/distill.ts +220 -220
- package/src/compression/events.ts +275 -275
- package/src/compression/markdown.ts +53 -53
- package/src/compression/normalize.ts +114 -114
- package/src/compression/render.ts +70 -70
- package/src/compression/schema.ts +84 -84
- package/src/compression/select.ts +123 -123
- package/src/compression/session-message.ts +112 -112
- package/src/compression/settings-rpc.ts +111 -111
- package/src/compression/settings.ts +175 -175
- package/src/compression/state.ts +248 -248
- package/src/compression/summarize.ts +595 -595
- package/src/compression/summary.ts +73 -73
- package/src/compression/tool.ts +142 -142
- package/src/compression/transcript-store.ts +258 -258
- package/src/compression/transcript.ts +63 -63
- package/src/compression/validate.ts +81 -81
- package/src/i18n/en.ts +93 -93
- package/src/i18n/index.ts +28 -28
- package/src/i18n/types.ts +89 -89
- package/src/i18n/zh.ts +93 -93
- package/src/index.ts +108 -108
- package/src/shared/blocks.ts +17 -17
- package/src/shared/cache.ts +199 -199
- package/src/shared/format.ts +23 -23
- package/src/shared/media.ts +184 -184
- package/src/shared/parse.ts +25 -25
- package/src/shared/parts.ts +72 -72
- package/src/shared/rpc.ts +145 -145
- package/src/shared/scan.ts +32 -32
- package/src/shared/settings.ts +471 -471
- package/src/shared/stats.ts +142 -142
- package/src/shared/tokens.ts +259 -259
- package/src/tui/chart/cache-chart.ts +237 -237
- package/src/tui/chart/color.ts +58 -58
- package/src/tui/index.tsx +111 -111
- package/src/tui/panel/cache.tsx +203 -203
- package/src/tui/panel/data.ts +105 -105
- package/src/tui/panel/index.tsx +176 -176
- package/src/tui/panel/summaries.tsx +192 -192
- package/src/tui/settings/dialogs.tsx +215 -215
- package/src/tui/settings/options.ts +238 -238
- package/src/tui/settings.tsx +418 -418
- 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
|
+
}
|