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,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
+ }