compressed-context 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +59 -51
  2. package/package.json +64 -64
  3. package/src/compression/active-summary-cache.ts +50 -50
  4. package/src/compression/adopt.ts +57 -57
  5. package/src/compression/auto.ts +386 -372
  6. package/src/compression/babel.ts +85 -85
  7. package/src/compression/block.ts +25 -25
  8. package/src/compression/cache-history.ts +75 -75
  9. package/src/compression/compaction.ts +189 -189
  10. package/src/compression/decompress-tool.ts +74 -74
  11. package/src/compression/decompress.ts +292 -292
  12. package/src/compression/deleted-sessions.ts +24 -24
  13. package/src/compression/distill.ts +220 -220
  14. package/src/compression/events.ts +275 -275
  15. package/src/compression/markdown.ts +53 -53
  16. package/src/compression/normalize.ts +114 -114
  17. package/src/compression/render.ts +70 -70
  18. package/src/compression/schema.ts +131 -84
  19. package/src/compression/select.ts +123 -123
  20. package/src/compression/session-message.ts +112 -112
  21. package/src/compression/settings-rpc.ts +123 -111
  22. package/src/compression/settings.ts +175 -175
  23. package/src/compression/shape.ts +75 -0
  24. package/src/compression/state.ts +248 -248
  25. package/src/compression/summarize.ts +534 -595
  26. package/src/compression/summary.ts +73 -73
  27. package/src/compression/tool.ts +184 -142
  28. package/src/compression/transcript-store.ts +258 -258
  29. package/src/compression/transcript.ts +63 -63
  30. package/src/compression/validate.ts +81 -81
  31. package/src/i18n/en.ts +93 -93
  32. package/src/i18n/index.ts +28 -28
  33. package/src/i18n/types.ts +89 -89
  34. package/src/i18n/zh.ts +93 -93
  35. package/src/index.ts +116 -108
  36. package/src/shared/blocks.ts +17 -17
  37. package/src/shared/cache.ts +199 -199
  38. package/src/shared/format.ts +23 -23
  39. package/src/shared/media.ts +184 -184
  40. package/src/shared/parse.ts +25 -25
  41. package/src/shared/parts.ts +72 -72
  42. package/src/shared/rpc.ts +145 -145
  43. package/src/shared/scan.ts +32 -32
  44. package/src/shared/settings.ts +466 -471
  45. package/src/shared/stats.ts +142 -142
  46. package/src/shared/tokens.ts +259 -259
  47. package/src/tui/chart/cache-chart.ts +237 -237
  48. package/src/tui/chart/color.ts +58 -58
  49. package/src/tui/index.tsx +109 -111
  50. package/src/tui/panel/cache.tsx +203 -203
  51. package/src/tui/panel/data.ts +105 -105
  52. package/src/tui/panel/index.tsx +176 -176
  53. package/src/tui/panel/summaries.tsx +203 -192
  54. package/src/tui/settings/dialogs.tsx +215 -215
  55. package/src/tui/settings/options.ts +238 -238
  56. package/src/tui/settings.tsx +418 -418
  57. package/src/tui/types.ts +4 -4
@@ -1,259 +1,259 @@
1
- import { attachmentTokens } from "./media.ts"
2
- import { stringify, toolInput, toolResult, toolResultText } from "./parts.ts"
3
-
4
- /**
5
- * 上下文分类 token 估算。
6
- *
7
- * 输入:`context` 钩子里的 system / tools / messages 原始结构。
8
- * 输出:各类别的 token 估算值(system、tools、tool、text、code、summaries、media)。
9
- * 预期:只用于计算「各类占多少比例」;绝对总量以 provider 返回的 usage 为准,
10
- * 展示层按真实总量缩放,所以这里的绝对值允许有偏差。
11
- */
12
-
13
- /** 单条消息内容的最小结构,避免绑定 @opencode/ai 的具体类。 */
14
- export interface FileAttachmentLike {
15
- readonly mime?: string | undefined
16
- readonly data?: string | Uint8Array | undefined
17
- readonly name?: string | undefined
18
- readonly uri?: string | undefined
19
- readonly durationSeconds?: number | undefined
20
- }
21
-
22
- export interface ContentPartLike {
23
- readonly type: string
24
- readonly text?: string | null | undefined
25
- readonly name?: string | undefined
26
- readonly input?: unknown
27
- readonly result?: unknown
28
- readonly state?:
29
- | {
30
- readonly input?: unknown
31
- readonly content?: unknown
32
- readonly output?: unknown
33
- }
34
- | undefined
35
- readonly file?: FileAttachmentLike | undefined
36
- readonly media?:
37
- | {
38
- readonly source?:
39
- | {
40
- readonly type?: string | undefined
41
- readonly data?: string | Uint8Array | undefined
42
- readonly mediaType?: string | undefined
43
- }
44
- | undefined
45
- readonly info?:
46
- | {
47
- readonly width?: number | undefined
48
- readonly height?: number | undefined
49
- readonly durationSeconds?: number | undefined
50
- }
51
- | undefined
52
- }
53
- | undefined
54
- }
55
-
56
- export interface MessageLike {
57
- readonly role?: string
58
- readonly content: readonly ContentPartLike[]
59
- /** @opencode/ai 的 Message 用 content;保留 text 兼容旧的扁平静态消息。 */
60
- readonly text?: string | null | undefined
61
- }
62
-
63
- export interface SystemPartLike {
64
- readonly type: string
65
- readonly text: string
66
- }
67
-
68
- export interface ToolDefinitionLike {
69
- readonly description?: string
70
- readonly input?: unknown
71
- }
72
-
73
- export interface TokenCategories {
74
- readonly system: number
75
- readonly tools: number
76
- readonly tool: number
77
- readonly text: number
78
- readonly recent: number
79
- readonly summaries: number
80
- readonly media: number
81
- }
82
-
83
- /** CJK 字符按 1 token 计,其余按 4 字符 1 token 向上取整。 */
84
- export function countText(text: string): number {
85
- if (text.length === 0) return 0
86
- const cjk = text.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/gu)
87
- const cjkCount = cjk?.length ?? 0
88
- return cjkCount + Math.ceil((text.length - cjkCount) / 4)
89
- }
90
-
91
- /** 输入:工具结果里的单个 content 项;输出:token 估算。 */
92
- function countToolResultItem(item: unknown): number {
93
- if (typeof item === "object" && item !== null) {
94
- const typed = item as {
95
- readonly type?: unknown
96
- readonly text?: unknown
97
- readonly mime?: string
98
- readonly uri?: string
99
- readonly name?: string
100
- readonly media?:
101
- | {
102
- readonly mediaType?: string
103
- readonly durationSeconds?: number
104
- readonly source?: { readonly data?: string | Uint8Array; readonly mediaType?: string }
105
- }
106
- | undefined
107
- }
108
- if (typed.type === "text" && typeof typed.text === "string") return countText(typed.text)
109
- if (typed.type === "file") {
110
- return attachmentTokens({ mime: typed.mime })
111
- }
112
- if (typed.type === "media") {
113
- return attachmentTokens({
114
- mime: typed.media?.mediaType ?? typed.media?.source?.mediaType,
115
- data: typed.media?.source?.data,
116
- durationSeconds: typed.media?.durationSeconds,
117
- })
118
- }
119
- }
120
- return countText(toolResultText(item))
121
- }
122
-
123
- /** 输入:工具结果;输出:token 估算;预期:v2 的 content 数组和旧 result 都能计数。 */
124
- function countToolResult(result: unknown): number {
125
- if (Array.isArray(result)) {
126
- return result.reduce<number>((sum, item) => sum + countToolResultItem(item), 0)
127
- }
128
- if (typeof result === "object" && result !== null) {
129
- const typed = result as { readonly content?: unknown; readonly value?: unknown; readonly output?: unknown }
130
- if (Array.isArray(typed.content)) return countToolResult(typed.content)
131
- if (Array.isArray(typed.value)) return countToolResult(typed.value)
132
- if (typed.output !== undefined) return countToolResult(typed.output)
133
- }
134
- return countText(toolResultText(result))
135
- }
136
-
137
- const CHECKPOINT_PREFIX = "<conversation-checkpoint>"
138
- const SUMMARY_TAG = /<summary>([\s\S]*?)<\/summary>/
139
- const RECENT_TAG = /<recent-context>([\s\S]*?)<\/recent-context>/
140
-
141
- /**
142
- * 输入:checkpoint 文本。
143
- * 输出:摘要与最近上下文两部分的 token 估算。
144
- * 预期:只把 <summary> 计入摘要;<recent-context> 是逐字保留的最近对话,
145
- * 单独计数,避免把最近上下文误当成摘要。
146
- */
147
- function countCheckpoint(text: string): { readonly summary: number; readonly recent: number } {
148
- const summary = SUMMARY_TAG.exec(text)?.[1]
149
- const recent = RECENT_TAG.exec(text)?.[1]
150
- if (summary === undefined && recent === undefined) {
151
- return { summary: countText(text), recent: 0 }
152
- }
153
- return { summary: countText(summary ?? ""), recent: countText(recent ?? "") }
154
- }
155
-
156
- /**
157
- * 输入:system 片段、工具定义表、消息列表。
158
- * 输出:按类别汇总的 token 估算。
159
- * 预期:所有 text / reasoning 都归入 text(代码块不再单列);tool-call 与 tool-result 归入 tool。
160
- */
161
- export function estimateCategories(input: {
162
- readonly system: readonly SystemPartLike[]
163
- readonly tools: Readonly<Record<string, ToolDefinitionLike>>
164
- readonly messages: readonly MessageLike[]
165
- }): TokenCategories {
166
- let system = 0
167
- for (const part of input.system) {
168
- if (part.type === "text") system += countText(part.text)
169
- }
170
-
171
- let tools = 0
172
- for (const [name, definition] of Object.entries(input.tools)) {
173
- tools += countText(`${name}${definition.description ?? ""}${stringify(definition.input)}`)
174
- }
175
-
176
- let tool = 0
177
- let text = 0
178
- let recent = 0
179
- let summaries = 0
180
- let media = 0
181
-
182
- for (const message of input.messages) {
183
- const content: readonly ContentPartLike[] = Array.isArray(message.content)
184
- ? message.content
185
- : []
186
- const contentHasText = content.some((part) => part.type === "text")
187
- if (!contentHasText && typeof message.text === "string") text += countText(message.text)
188
- for (const part of content) {
189
- switch (part.type) {
190
- case "text": {
191
- const value = part.text ?? ""
192
- if (value.trimStart().startsWith(CHECKPOINT_PREFIX)) {
193
- const checkpoint = countCheckpoint(value)
194
- summaries += checkpoint.summary
195
- recent += checkpoint.recent
196
- } else text += countText(value)
197
- break
198
- }
199
- case "reasoning": {
200
- text += countText(part.text ?? "")
201
- break
202
- }
203
- case "tool-call": {
204
- tool += countText(`${part.name ?? ""}${stringify(toolInput(part))}`)
205
- break
206
- }
207
- case "tool-result": {
208
- tool += countToolResult(toolResult(part))
209
- break
210
- }
211
- case "tool": {
212
- tool += countText(`${part.name ?? ""}${stringify(toolInput(part))}`)
213
- tool += countToolResult(toolResult(part))
214
- break
215
- }
216
- case "compaction": {
217
- summaries += countText(part.text ?? "")
218
- break
219
- }
220
- case "file": {
221
- media += attachmentTokens({
222
- mime: part.file?.mime,
223
- data: part.file?.data,
224
- durationSeconds: part.file?.durationSeconds,
225
- })
226
- break
227
- }
228
- case "media": {
229
- media += attachmentTokens({
230
- mime: part.media?.source?.mediaType,
231
- data: part.media?.source?.data,
232
- width: part.media?.info?.width,
233
- height: part.media?.info?.height,
234
- durationSeconds: part.media?.info?.durationSeconds,
235
- })
236
- break
237
- }
238
- default: {
239
- break
240
- }
241
- }
242
- }
243
- }
244
-
245
- return { system, tools, tool, text, recent, summaries, media }
246
- }
247
-
248
- /** 输入:分类估算;输出:各类之和。 */
249
- export function totalCategories(categories: TokenCategories): number {
250
- return (
251
- categories.system +
252
- categories.tools +
253
- categories.tool +
254
- categories.text +
255
- categories.recent +
256
- categories.summaries +
257
- categories.media
258
- )
259
- }
1
+ import { attachmentTokens } from "./media.ts"
2
+ import { stringify, toolInput, toolResult, toolResultText } from "./parts.ts"
3
+
4
+ /**
5
+ * 上下文分类 token 估算。
6
+ *
7
+ * 输入:`context` 钩子里的 system / tools / messages 原始结构。
8
+ * 输出:各类别的 token 估算值(system、tools、tool、text、code、summaries、media)。
9
+ * 预期:只用于计算「各类占多少比例」;绝对总量以 provider 返回的 usage 为准,
10
+ * 展示层按真实总量缩放,所以这里的绝对值允许有偏差。
11
+ */
12
+
13
+ /** 单条消息内容的最小结构,避免绑定 @opencode/ai 的具体类。 */
14
+ export interface FileAttachmentLike {
15
+ readonly mime?: string | undefined
16
+ readonly data?: string | Uint8Array | undefined
17
+ readonly name?: string | undefined
18
+ readonly uri?: string | undefined
19
+ readonly durationSeconds?: number | undefined
20
+ }
21
+
22
+ export interface ContentPartLike {
23
+ readonly type: string
24
+ readonly text?: string | null | undefined
25
+ readonly name?: string | undefined
26
+ readonly input?: unknown
27
+ readonly result?: unknown
28
+ readonly state?:
29
+ | {
30
+ readonly input?: unknown
31
+ readonly content?: unknown
32
+ readonly output?: unknown
33
+ }
34
+ | undefined
35
+ readonly file?: FileAttachmentLike | undefined
36
+ readonly media?:
37
+ | {
38
+ readonly source?:
39
+ | {
40
+ readonly type?: string | undefined
41
+ readonly data?: string | Uint8Array | undefined
42
+ readonly mediaType?: string | undefined
43
+ }
44
+ | undefined
45
+ readonly info?:
46
+ | {
47
+ readonly width?: number | undefined
48
+ readonly height?: number | undefined
49
+ readonly durationSeconds?: number | undefined
50
+ }
51
+ | undefined
52
+ }
53
+ | undefined
54
+ }
55
+
56
+ export interface MessageLike {
57
+ readonly role?: string
58
+ readonly content: readonly ContentPartLike[]
59
+ /** @opencode/ai 的 Message 用 content;保留 text 兼容旧的扁平静态消息。 */
60
+ readonly text?: string | null | undefined
61
+ }
62
+
63
+ export interface SystemPartLike {
64
+ readonly type: string
65
+ readonly text: string
66
+ }
67
+
68
+ export interface ToolDefinitionLike {
69
+ readonly description?: string
70
+ readonly input?: unknown
71
+ }
72
+
73
+ export interface TokenCategories {
74
+ readonly system: number
75
+ readonly tools: number
76
+ readonly tool: number
77
+ readonly text: number
78
+ readonly recent: number
79
+ readonly summaries: number
80
+ readonly media: number
81
+ }
82
+
83
+ /** CJK 字符按 1 token 计,其余按 4 字符 1 token 向上取整。 */
84
+ export function countText(text: string): number {
85
+ if (text.length === 0) return 0
86
+ const cjk = text.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/gu)
87
+ const cjkCount = cjk?.length ?? 0
88
+ return cjkCount + Math.ceil((text.length - cjkCount) / 4)
89
+ }
90
+
91
+ /** 输入:工具结果里的单个 content 项;输出:token 估算。 */
92
+ function countToolResultItem(item: unknown): number {
93
+ if (typeof item === "object" && item !== null) {
94
+ const typed = item as {
95
+ readonly type?: unknown
96
+ readonly text?: unknown
97
+ readonly mime?: string
98
+ readonly uri?: string
99
+ readonly name?: string
100
+ readonly media?:
101
+ | {
102
+ readonly mediaType?: string
103
+ readonly durationSeconds?: number
104
+ readonly source?: { readonly data?: string | Uint8Array; readonly mediaType?: string }
105
+ }
106
+ | undefined
107
+ }
108
+ if (typed.type === "text" && typeof typed.text === "string") return countText(typed.text)
109
+ if (typed.type === "file") {
110
+ return attachmentTokens({ mime: typed.mime })
111
+ }
112
+ if (typed.type === "media") {
113
+ return attachmentTokens({
114
+ mime: typed.media?.mediaType ?? typed.media?.source?.mediaType,
115
+ data: typed.media?.source?.data,
116
+ durationSeconds: typed.media?.durationSeconds,
117
+ })
118
+ }
119
+ }
120
+ return countText(toolResultText(item))
121
+ }
122
+
123
+ /** 输入:工具结果;输出:token 估算;预期:v2 的 content 数组和旧 result 都能计数。 */
124
+ function countToolResult(result: unknown): number {
125
+ if (Array.isArray(result)) {
126
+ return result.reduce<number>((sum, item) => sum + countToolResultItem(item), 0)
127
+ }
128
+ if (typeof result === "object" && result !== null) {
129
+ const typed = result as { readonly content?: unknown; readonly value?: unknown; readonly output?: unknown }
130
+ if (Array.isArray(typed.content)) return countToolResult(typed.content)
131
+ if (Array.isArray(typed.value)) return countToolResult(typed.value)
132
+ if (typed.output !== undefined) return countToolResult(typed.output)
133
+ }
134
+ return countText(toolResultText(result))
135
+ }
136
+
137
+ const CHECKPOINT_PREFIX = "<conversation-checkpoint>"
138
+ const SUMMARY_TAG = /<summary>([\s\S]*?)<\/summary>/
139
+ const RECENT_TAG = /<recent-context>([\s\S]*?)<\/recent-context>/
140
+
141
+ /**
142
+ * 输入:checkpoint 文本。
143
+ * 输出:摘要与最近上下文两部分的 token 估算。
144
+ * 预期:只把 <summary> 计入摘要;<recent-context> 是逐字保留的最近对话,
145
+ * 单独计数,避免把最近上下文误当成摘要。
146
+ */
147
+ function countCheckpoint(text: string): { readonly summary: number; readonly recent: number } {
148
+ const summary = SUMMARY_TAG.exec(text)?.[1]
149
+ const recent = RECENT_TAG.exec(text)?.[1]
150
+ if (summary === undefined && recent === undefined) {
151
+ return { summary: countText(text), recent: 0 }
152
+ }
153
+ return { summary: countText(summary ?? ""), recent: countText(recent ?? "") }
154
+ }
155
+
156
+ /**
157
+ * 输入:system 片段、工具定义表、消息列表。
158
+ * 输出:按类别汇总的 token 估算。
159
+ * 预期:所有 text / reasoning 都归入 text(代码块不再单列);tool-call 与 tool-result 归入 tool。
160
+ */
161
+ export function estimateCategories(input: {
162
+ readonly system: readonly SystemPartLike[]
163
+ readonly tools: Readonly<Record<string, ToolDefinitionLike>>
164
+ readonly messages: readonly MessageLike[]
165
+ }): TokenCategories {
166
+ let system = 0
167
+ for (const part of input.system) {
168
+ if (part.type === "text") system += countText(part.text)
169
+ }
170
+
171
+ let tools = 0
172
+ for (const [name, definition] of Object.entries(input.tools)) {
173
+ tools += countText(`${name}${definition.description ?? ""}${stringify(definition.input)}`)
174
+ }
175
+
176
+ let tool = 0
177
+ let text = 0
178
+ let recent = 0
179
+ let summaries = 0
180
+ let media = 0
181
+
182
+ for (const message of input.messages) {
183
+ const content: readonly ContentPartLike[] = Array.isArray(message.content)
184
+ ? message.content
185
+ : []
186
+ const contentHasText = content.some((part) => part.type === "text")
187
+ if (!contentHasText && typeof message.text === "string") text += countText(message.text)
188
+ for (const part of content) {
189
+ switch (part.type) {
190
+ case "text": {
191
+ const value = part.text ?? ""
192
+ if (value.trimStart().startsWith(CHECKPOINT_PREFIX)) {
193
+ const checkpoint = countCheckpoint(value)
194
+ summaries += checkpoint.summary
195
+ recent += checkpoint.recent
196
+ } else text += countText(value)
197
+ break
198
+ }
199
+ case "reasoning": {
200
+ text += countText(part.text ?? "")
201
+ break
202
+ }
203
+ case "tool-call": {
204
+ tool += countText(`${part.name ?? ""}${stringify(toolInput(part))}`)
205
+ break
206
+ }
207
+ case "tool-result": {
208
+ tool += countToolResult(toolResult(part))
209
+ break
210
+ }
211
+ case "tool": {
212
+ tool += countText(`${part.name ?? ""}${stringify(toolInput(part))}`)
213
+ tool += countToolResult(toolResult(part))
214
+ break
215
+ }
216
+ case "compaction": {
217
+ summaries += countText(part.text ?? "")
218
+ break
219
+ }
220
+ case "file": {
221
+ media += attachmentTokens({
222
+ mime: part.file?.mime,
223
+ data: part.file?.data,
224
+ durationSeconds: part.file?.durationSeconds,
225
+ })
226
+ break
227
+ }
228
+ case "media": {
229
+ media += attachmentTokens({
230
+ mime: part.media?.source?.mediaType,
231
+ data: part.media?.source?.data,
232
+ width: part.media?.info?.width,
233
+ height: part.media?.info?.height,
234
+ durationSeconds: part.media?.info?.durationSeconds,
235
+ })
236
+ break
237
+ }
238
+ default: {
239
+ break
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ return { system, tools, tool, text, recent, summaries, media }
246
+ }
247
+
248
+ /** 输入:分类估算;输出:各类之和。 */
249
+ export function totalCategories(categories: TokenCategories): number {
250
+ return (
251
+ categories.system +
252
+ categories.tools +
253
+ categories.tool +
254
+ categories.text +
255
+ categories.recent +
256
+ categories.summaries +
257
+ categories.media
258
+ )
259
+ }