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.
- package/README.md +59 -51
- 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 +386 -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 +131 -84
- package/src/compression/select.ts +123 -123
- package/src/compression/session-message.ts +112 -112
- package/src/compression/settings-rpc.ts +123 -111
- package/src/compression/settings.ts +175 -175
- package/src/compression/shape.ts +75 -0
- package/src/compression/state.ts +248 -248
- package/src/compression/summarize.ts +534 -595
- package/src/compression/summary.ts +73 -73
- package/src/compression/tool.ts +184 -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 +116 -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 +466 -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 +109 -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 +203 -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,258 +1,258 @@
|
|
|
1
|
-
import type { Schema } from "effect"
|
|
2
|
-
|
|
3
|
-
import { isSessionDeleted } from "./deleted-sessions.ts"
|
|
4
|
-
import { forEachScannedEntry } from "../shared/scan.ts"
|
|
5
|
-
import type { Block, StorageLike } from "./state.ts"
|
|
6
|
-
import type { MediaSourceLike, MessageLike } from "./select.ts"
|
|
7
|
-
|
|
8
|
-
/** 落盘的媒体 source:bytes 统一转 base64;url/ref 只存引用。 */
|
|
9
|
-
export interface StoredMediaSource {
|
|
10
|
-
readonly type: "base64" | "url" | "ref" | "unknown"
|
|
11
|
-
readonly data?: string
|
|
12
|
-
readonly mediaType?: string
|
|
13
|
-
readonly url?: string
|
|
14
|
-
readonly provider?: string
|
|
15
|
-
readonly id?: string
|
|
16
|
-
readonly expiresAt?: number
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** 落盘的单条 part;reasoning 不进入这里。 */
|
|
20
|
-
export interface StoredPart {
|
|
21
|
-
readonly type: "text" | "tool-call" | "tool-result" | "media"
|
|
22
|
-
readonly text?: string
|
|
23
|
-
readonly name?: string
|
|
24
|
-
readonly input?: unknown
|
|
25
|
-
readonly result?: unknown
|
|
26
|
-
readonly filename?: string
|
|
27
|
-
readonly media?: {
|
|
28
|
-
readonly source: StoredMediaSource
|
|
29
|
-
readonly info?: unknown
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** 落盘的单条消息。 */
|
|
34
|
-
export interface StoredMessage {
|
|
35
|
-
readonly id?: string
|
|
36
|
-
readonly role: string
|
|
37
|
-
readonly content: readonly StoredPart[]
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** 一个 T1 块的 transcript manifest;消息本体按 msg:<index> 分开存。 */
|
|
41
|
-
export interface TranscriptManifest {
|
|
42
|
-
readonly v: 1
|
|
43
|
-
readonly sessionID: string
|
|
44
|
-
readonly blockID: string
|
|
45
|
-
readonly fromId: string
|
|
46
|
-
readonly toId: string
|
|
47
|
-
readonly count: number
|
|
48
|
-
readonly createdAt: number
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const PREFIX = "compression:transcript:"
|
|
52
|
-
|
|
53
|
-
/** 输入:sessionID;输出:该会话全部 transcript key 的公共前缀。 */
|
|
54
|
-
export function transcriptSessionPrefix(sessionID: string): string {
|
|
55
|
-
return `${PREFIX}${sessionID}:`
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** 输入:sessionID + blockID;输出:manifest key。 */
|
|
59
|
-
export function transcriptManifestKey(sessionID: string, blockID: string): string {
|
|
60
|
-
return `${transcriptSessionPrefix(sessionID)}${blockID}:manifest`
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** 输入:sessionID + blockID + 序号;输出:单条消息 key。 */
|
|
64
|
-
export function transcriptMessageKey(sessionID: string, blockID: string, index: number): string {
|
|
65
|
-
return `${transcriptSessionPrefix(sessionID)}${blockID}:msg:${index.toString()}`
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** 输入:任意数据;输出:是否 Uint8Array。 */
|
|
69
|
-
function isBytes(value: unknown): value is Uint8Array {
|
|
70
|
-
return value instanceof Uint8Array
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** 输入:media source;输出:可 JSON 化的 source;未知形状保留 unknown。 */
|
|
74
|
-
function storeSource(source: MediaSourceLike | undefined): StoredMediaSource {
|
|
75
|
-
if (source === undefined) return { type: "unknown" }
|
|
76
|
-
if (source.type === "bytes") {
|
|
77
|
-
const data = isBytes(source.data)
|
|
78
|
-
? Buffer.from(source.data).toString("base64")
|
|
79
|
-
: typeof source.data === "string"
|
|
80
|
-
? source.data
|
|
81
|
-
: ""
|
|
82
|
-
return {
|
|
83
|
-
type: "base64",
|
|
84
|
-
...(data.length === 0 ? {} : { data }),
|
|
85
|
-
...(source.mediaType === undefined ? {} : { mediaType: source.mediaType }),
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (source.type === "base64") {
|
|
89
|
-
return {
|
|
90
|
-
type: "base64",
|
|
91
|
-
...(typeof source.data === "string" ? { data: source.data } : {}),
|
|
92
|
-
...(source.mediaType === undefined ? {} : { mediaType: source.mediaType }),
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
if (source.type === "url") {
|
|
96
|
-
return {
|
|
97
|
-
type: "url",
|
|
98
|
-
...(source.url === undefined ? {} : { url: source.url }),
|
|
99
|
-
...(source.mediaType === undefined ? {} : { mediaType: source.mediaType }),
|
|
100
|
-
...(source.expiresAt === undefined ? {} : { expiresAt: source.expiresAt }),
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
if (source.type === "ref") {
|
|
104
|
-
return {
|
|
105
|
-
type: "ref",
|
|
106
|
-
...(source.provider === undefined ? {} : { provider: source.provider }),
|
|
107
|
-
...(source.id === undefined ? {} : { id: source.id }),
|
|
108
|
-
...(source.mediaType === undefined ? {} : { mediaType: source.mediaType }),
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return { type: "unknown", ...(source.mediaType === undefined ? {} : { mediaType: source.mediaType }) }
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** 输入:可压缩消息;输出:落盘消息;预期:跳过 reasoning 和其他未知 part。 */
|
|
115
|
-
export function toStoredMessage(message: MessageLike): StoredMessage {
|
|
116
|
-
const content: StoredPart[] = []
|
|
117
|
-
for (const part of message.content) {
|
|
118
|
-
if (part.type === "text" && typeof part.text === "string") {
|
|
119
|
-
content.push({ type: "text", text: part.text })
|
|
120
|
-
continue
|
|
121
|
-
}
|
|
122
|
-
if (part.type === "tool-call") {
|
|
123
|
-
content.push({
|
|
124
|
-
type: "tool-call",
|
|
125
|
-
...(part.name === undefined ? {} : { name: part.name }),
|
|
126
|
-
input: part.input,
|
|
127
|
-
})
|
|
128
|
-
continue
|
|
129
|
-
}
|
|
130
|
-
if (part.type === "tool-result") {
|
|
131
|
-
content.push({
|
|
132
|
-
type: "tool-result",
|
|
133
|
-
...(part.name === undefined ? {} : { name: part.name }),
|
|
134
|
-
result: part.result,
|
|
135
|
-
})
|
|
136
|
-
continue
|
|
137
|
-
}
|
|
138
|
-
if (part.type === "media") {
|
|
139
|
-
content.push({
|
|
140
|
-
type: "media",
|
|
141
|
-
...(part.filename === undefined ? {} : { filename: part.filename }),
|
|
142
|
-
media: {
|
|
143
|
-
source: storeSource(part.media?.source),
|
|
144
|
-
...(part.media?.info === undefined ? {} : { info: part.media.info }),
|
|
145
|
-
},
|
|
146
|
-
})
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return {
|
|
150
|
-
...(message.id === undefined ? {} : { id: message.id }),
|
|
151
|
-
role: message.role,
|
|
152
|
-
content,
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* 输入:storage + 会话 + T1 块 + 覆盖的消息。
|
|
158
|
-
* 输出:无。
|
|
159
|
-
* 预期:先写 manifest,再逐条写消息;删除墓碑存在时整体跳过。
|
|
160
|
-
*/
|
|
161
|
-
export async function saveTranscript(
|
|
162
|
-
storage: StorageLike,
|
|
163
|
-
sessionID: string,
|
|
164
|
-
block: Pick<Block, "id" | "fromId" | "toId">,
|
|
165
|
-
messages: readonly MessageLike[],
|
|
166
|
-
): Promise<void> {
|
|
167
|
-
if (isSessionDeleted(sessionID)) return
|
|
168
|
-
const manifest: TranscriptManifest = {
|
|
169
|
-
v: 1,
|
|
170
|
-
sessionID,
|
|
171
|
-
blockID: block.id,
|
|
172
|
-
fromId: block.fromId,
|
|
173
|
-
toId: block.toId,
|
|
174
|
-
count: messages.length,
|
|
175
|
-
createdAt: Date.now(),
|
|
176
|
-
}
|
|
177
|
-
await storage.set(transcriptManifestKey(sessionID, block.id), manifest as unknown as Schema.Json)
|
|
178
|
-
for (const [index, message] of messages.entries()) {
|
|
179
|
-
await storage.set(
|
|
180
|
-
transcriptMessageKey(sessionID, block.id, index),
|
|
181
|
-
toStoredMessage(message) as unknown as Schema.Json,
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** 输入:storage + 会话 + 块;输出:合法 manifest 或 undefined。 */
|
|
187
|
-
export async function loadTranscriptManifest(
|
|
188
|
-
storage: StorageLike,
|
|
189
|
-
sessionID: string,
|
|
190
|
-
blockID: string,
|
|
191
|
-
): Promise<TranscriptManifest | undefined> {
|
|
192
|
-
const raw = await storage.get(transcriptManifestKey(sessionID, blockID))
|
|
193
|
-
if (typeof raw !== "object" || raw === null) return undefined
|
|
194
|
-
const record = raw as Record<string, unknown>
|
|
195
|
-
if (
|
|
196
|
-
record["v"] !== 1 ||
|
|
197
|
-
typeof record["sessionID"] !== "string" ||
|
|
198
|
-
typeof record["blockID"] !== "string" ||
|
|
199
|
-
typeof record["fromId"] !== "string" ||
|
|
200
|
-
typeof record["toId"] !== "string" ||
|
|
201
|
-
typeof record["count"] !== "number" ||
|
|
202
|
-
!Number.isInteger(record["count"]) ||
|
|
203
|
-
record["count"] < 0
|
|
204
|
-
) {
|
|
205
|
-
return undefined
|
|
206
|
-
}
|
|
207
|
-
return {
|
|
208
|
-
v: 1,
|
|
209
|
-
sessionID: record["sessionID"],
|
|
210
|
-
blockID: record["blockID"],
|
|
211
|
-
fromId: record["fromId"],
|
|
212
|
-
toId: record["toId"],
|
|
213
|
-
count: record["count"],
|
|
214
|
-
createdAt: typeof record["createdAt"] === "number" ? record["createdAt"] : 0,
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** 输入:storage + 会话 + 块 + 序号;输出:落盘消息或 undefined。 */
|
|
219
|
-
export async function loadTranscriptMessage(
|
|
220
|
-
storage: StorageLike,
|
|
221
|
-
sessionID: string,
|
|
222
|
-
blockID: string,
|
|
223
|
-
index: number,
|
|
224
|
-
): Promise<StoredMessage | undefined> {
|
|
225
|
-
const raw = await storage.get(transcriptMessageKey(sessionID, blockID, index))
|
|
226
|
-
if (typeof raw !== "object" || raw === null) return undefined
|
|
227
|
-
const record = raw as Record<string, unknown>
|
|
228
|
-
if (typeof record["role"] !== "string" || !Array.isArray(record["content"])) return undefined
|
|
229
|
-
return {
|
|
230
|
-
...(typeof record["id"] === "string" ? { id: record["id"] } : {}),
|
|
231
|
-
role: record["role"],
|
|
232
|
-
content: record["content"] as StoredPart[],
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/** 输入:storage + 会话;输出:无;预期:删掉该会话所有 manifest/message key。 */
|
|
237
|
-
export async function deleteTranscriptsForSession(
|
|
238
|
-
storage: StorageLike & {
|
|
239
|
-
scan?: (options: { prefix: string; limit: number; after?: string }) => Promise<{
|
|
240
|
-
entries: readonly { key: string; value: unknown }[]
|
|
241
|
-
next?: string
|
|
242
|
-
}>
|
|
243
|
-
},
|
|
244
|
-
sessionID: string,
|
|
245
|
-
): Promise<void> {
|
|
246
|
-
await forEachScannedEntry(storage, transcriptSessionPrefix(sessionID), async (entry) => {
|
|
247
|
-
await storage.remove(entry.key)
|
|
248
|
-
})
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/** 输入:storage + 会话 + 块;输出:manifest 是否存在。 */
|
|
252
|
-
export async function hasTranscript(
|
|
253
|
-
storage: StorageLike,
|
|
254
|
-
sessionID: string,
|
|
255
|
-
blockID: string,
|
|
256
|
-
): Promise<boolean> {
|
|
257
|
-
return (await loadTranscriptManifest(storage, sessionID, blockID)) !== undefined
|
|
258
|
-
}
|
|
1
|
+
import type { Schema } from "effect"
|
|
2
|
+
|
|
3
|
+
import { isSessionDeleted } from "./deleted-sessions.ts"
|
|
4
|
+
import { forEachScannedEntry } from "../shared/scan.ts"
|
|
5
|
+
import type { Block, StorageLike } from "./state.ts"
|
|
6
|
+
import type { MediaSourceLike, MessageLike } from "./select.ts"
|
|
7
|
+
|
|
8
|
+
/** 落盘的媒体 source:bytes 统一转 base64;url/ref 只存引用。 */
|
|
9
|
+
export interface StoredMediaSource {
|
|
10
|
+
readonly type: "base64" | "url" | "ref" | "unknown"
|
|
11
|
+
readonly data?: string
|
|
12
|
+
readonly mediaType?: string
|
|
13
|
+
readonly url?: string
|
|
14
|
+
readonly provider?: string
|
|
15
|
+
readonly id?: string
|
|
16
|
+
readonly expiresAt?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 落盘的单条 part;reasoning 不进入这里。 */
|
|
20
|
+
export interface StoredPart {
|
|
21
|
+
readonly type: "text" | "tool-call" | "tool-result" | "media"
|
|
22
|
+
readonly text?: string
|
|
23
|
+
readonly name?: string
|
|
24
|
+
readonly input?: unknown
|
|
25
|
+
readonly result?: unknown
|
|
26
|
+
readonly filename?: string
|
|
27
|
+
readonly media?: {
|
|
28
|
+
readonly source: StoredMediaSource
|
|
29
|
+
readonly info?: unknown
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 落盘的单条消息。 */
|
|
34
|
+
export interface StoredMessage {
|
|
35
|
+
readonly id?: string
|
|
36
|
+
readonly role: string
|
|
37
|
+
readonly content: readonly StoredPart[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 一个 T1 块的 transcript manifest;消息本体按 msg:<index> 分开存。 */
|
|
41
|
+
export interface TranscriptManifest {
|
|
42
|
+
readonly v: 1
|
|
43
|
+
readonly sessionID: string
|
|
44
|
+
readonly blockID: string
|
|
45
|
+
readonly fromId: string
|
|
46
|
+
readonly toId: string
|
|
47
|
+
readonly count: number
|
|
48
|
+
readonly createdAt: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const PREFIX = "compression:transcript:"
|
|
52
|
+
|
|
53
|
+
/** 输入:sessionID;输出:该会话全部 transcript key 的公共前缀。 */
|
|
54
|
+
export function transcriptSessionPrefix(sessionID: string): string {
|
|
55
|
+
return `${PREFIX}${sessionID}:`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 输入:sessionID + blockID;输出:manifest key。 */
|
|
59
|
+
export function transcriptManifestKey(sessionID: string, blockID: string): string {
|
|
60
|
+
return `${transcriptSessionPrefix(sessionID)}${blockID}:manifest`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 输入:sessionID + blockID + 序号;输出:单条消息 key。 */
|
|
64
|
+
export function transcriptMessageKey(sessionID: string, blockID: string, index: number): string {
|
|
65
|
+
return `${transcriptSessionPrefix(sessionID)}${blockID}:msg:${index.toString()}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 输入:任意数据;输出:是否 Uint8Array。 */
|
|
69
|
+
function isBytes(value: unknown): value is Uint8Array {
|
|
70
|
+
return value instanceof Uint8Array
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 输入:media source;输出:可 JSON 化的 source;未知形状保留 unknown。 */
|
|
74
|
+
function storeSource(source: MediaSourceLike | undefined): StoredMediaSource {
|
|
75
|
+
if (source === undefined) return { type: "unknown" }
|
|
76
|
+
if (source.type === "bytes") {
|
|
77
|
+
const data = isBytes(source.data)
|
|
78
|
+
? Buffer.from(source.data).toString("base64")
|
|
79
|
+
: typeof source.data === "string"
|
|
80
|
+
? source.data
|
|
81
|
+
: ""
|
|
82
|
+
return {
|
|
83
|
+
type: "base64",
|
|
84
|
+
...(data.length === 0 ? {} : { data }),
|
|
85
|
+
...(source.mediaType === undefined ? {} : { mediaType: source.mediaType }),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (source.type === "base64") {
|
|
89
|
+
return {
|
|
90
|
+
type: "base64",
|
|
91
|
+
...(typeof source.data === "string" ? { data: source.data } : {}),
|
|
92
|
+
...(source.mediaType === undefined ? {} : { mediaType: source.mediaType }),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (source.type === "url") {
|
|
96
|
+
return {
|
|
97
|
+
type: "url",
|
|
98
|
+
...(source.url === undefined ? {} : { url: source.url }),
|
|
99
|
+
...(source.mediaType === undefined ? {} : { mediaType: source.mediaType }),
|
|
100
|
+
...(source.expiresAt === undefined ? {} : { expiresAt: source.expiresAt }),
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (source.type === "ref") {
|
|
104
|
+
return {
|
|
105
|
+
type: "ref",
|
|
106
|
+
...(source.provider === undefined ? {} : { provider: source.provider }),
|
|
107
|
+
...(source.id === undefined ? {} : { id: source.id }),
|
|
108
|
+
...(source.mediaType === undefined ? {} : { mediaType: source.mediaType }),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return { type: "unknown", ...(source.mediaType === undefined ? {} : { mediaType: source.mediaType }) }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 输入:可压缩消息;输出:落盘消息;预期:跳过 reasoning 和其他未知 part。 */
|
|
115
|
+
export function toStoredMessage(message: MessageLike): StoredMessage {
|
|
116
|
+
const content: StoredPart[] = []
|
|
117
|
+
for (const part of message.content) {
|
|
118
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
119
|
+
content.push({ type: "text", text: part.text })
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
if (part.type === "tool-call") {
|
|
123
|
+
content.push({
|
|
124
|
+
type: "tool-call",
|
|
125
|
+
...(part.name === undefined ? {} : { name: part.name }),
|
|
126
|
+
input: part.input,
|
|
127
|
+
})
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
if (part.type === "tool-result") {
|
|
131
|
+
content.push({
|
|
132
|
+
type: "tool-result",
|
|
133
|
+
...(part.name === undefined ? {} : { name: part.name }),
|
|
134
|
+
result: part.result,
|
|
135
|
+
})
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
if (part.type === "media") {
|
|
139
|
+
content.push({
|
|
140
|
+
type: "media",
|
|
141
|
+
...(part.filename === undefined ? {} : { filename: part.filename }),
|
|
142
|
+
media: {
|
|
143
|
+
source: storeSource(part.media?.source),
|
|
144
|
+
...(part.media?.info === undefined ? {} : { info: part.media.info }),
|
|
145
|
+
},
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
...(message.id === undefined ? {} : { id: message.id }),
|
|
151
|
+
role: message.role,
|
|
152
|
+
content,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 输入:storage + 会话 + T1 块 + 覆盖的消息。
|
|
158
|
+
* 输出:无。
|
|
159
|
+
* 预期:先写 manifest,再逐条写消息;删除墓碑存在时整体跳过。
|
|
160
|
+
*/
|
|
161
|
+
export async function saveTranscript(
|
|
162
|
+
storage: StorageLike,
|
|
163
|
+
sessionID: string,
|
|
164
|
+
block: Pick<Block, "id" | "fromId" | "toId">,
|
|
165
|
+
messages: readonly MessageLike[],
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
if (isSessionDeleted(sessionID)) return
|
|
168
|
+
const manifest: TranscriptManifest = {
|
|
169
|
+
v: 1,
|
|
170
|
+
sessionID,
|
|
171
|
+
blockID: block.id,
|
|
172
|
+
fromId: block.fromId,
|
|
173
|
+
toId: block.toId,
|
|
174
|
+
count: messages.length,
|
|
175
|
+
createdAt: Date.now(),
|
|
176
|
+
}
|
|
177
|
+
await storage.set(transcriptManifestKey(sessionID, block.id), manifest as unknown as Schema.Json)
|
|
178
|
+
for (const [index, message] of messages.entries()) {
|
|
179
|
+
await storage.set(
|
|
180
|
+
transcriptMessageKey(sessionID, block.id, index),
|
|
181
|
+
toStoredMessage(message) as unknown as Schema.Json,
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 输入:storage + 会话 + 块;输出:合法 manifest 或 undefined。 */
|
|
187
|
+
export async function loadTranscriptManifest(
|
|
188
|
+
storage: StorageLike,
|
|
189
|
+
sessionID: string,
|
|
190
|
+
blockID: string,
|
|
191
|
+
): Promise<TranscriptManifest | undefined> {
|
|
192
|
+
const raw = await storage.get(transcriptManifestKey(sessionID, blockID))
|
|
193
|
+
if (typeof raw !== "object" || raw === null) return undefined
|
|
194
|
+
const record = raw as Record<string, unknown>
|
|
195
|
+
if (
|
|
196
|
+
record["v"] !== 1 ||
|
|
197
|
+
typeof record["sessionID"] !== "string" ||
|
|
198
|
+
typeof record["blockID"] !== "string" ||
|
|
199
|
+
typeof record["fromId"] !== "string" ||
|
|
200
|
+
typeof record["toId"] !== "string" ||
|
|
201
|
+
typeof record["count"] !== "number" ||
|
|
202
|
+
!Number.isInteger(record["count"]) ||
|
|
203
|
+
record["count"] < 0
|
|
204
|
+
) {
|
|
205
|
+
return undefined
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
v: 1,
|
|
209
|
+
sessionID: record["sessionID"],
|
|
210
|
+
blockID: record["blockID"],
|
|
211
|
+
fromId: record["fromId"],
|
|
212
|
+
toId: record["toId"],
|
|
213
|
+
count: record["count"],
|
|
214
|
+
createdAt: typeof record["createdAt"] === "number" ? record["createdAt"] : 0,
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 输入:storage + 会话 + 块 + 序号;输出:落盘消息或 undefined。 */
|
|
219
|
+
export async function loadTranscriptMessage(
|
|
220
|
+
storage: StorageLike,
|
|
221
|
+
sessionID: string,
|
|
222
|
+
blockID: string,
|
|
223
|
+
index: number,
|
|
224
|
+
): Promise<StoredMessage | undefined> {
|
|
225
|
+
const raw = await storage.get(transcriptMessageKey(sessionID, blockID, index))
|
|
226
|
+
if (typeof raw !== "object" || raw === null) return undefined
|
|
227
|
+
const record = raw as Record<string, unknown>
|
|
228
|
+
if (typeof record["role"] !== "string" || !Array.isArray(record["content"])) return undefined
|
|
229
|
+
return {
|
|
230
|
+
...(typeof record["id"] === "string" ? { id: record["id"] } : {}),
|
|
231
|
+
role: record["role"],
|
|
232
|
+
content: record["content"] as StoredPart[],
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** 输入:storage + 会话;输出:无;预期:删掉该会话所有 manifest/message key。 */
|
|
237
|
+
export async function deleteTranscriptsForSession(
|
|
238
|
+
storage: StorageLike & {
|
|
239
|
+
scan?: (options: { prefix: string; limit: number; after?: string }) => Promise<{
|
|
240
|
+
entries: readonly { key: string; value: unknown }[]
|
|
241
|
+
next?: string
|
|
242
|
+
}>
|
|
243
|
+
},
|
|
244
|
+
sessionID: string,
|
|
245
|
+
): Promise<void> {
|
|
246
|
+
await forEachScannedEntry(storage, transcriptSessionPrefix(sessionID), async (entry) => {
|
|
247
|
+
await storage.remove(entry.key)
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** 输入:storage + 会话 + 块;输出:manifest 是否存在。 */
|
|
252
|
+
export async function hasTranscript(
|
|
253
|
+
storage: StorageLike,
|
|
254
|
+
sessionID: string,
|
|
255
|
+
blockID: string,
|
|
256
|
+
): Promise<boolean> {
|
|
257
|
+
return (await loadTranscriptManifest(storage, sessionID, blockID)) !== undefined
|
|
258
|
+
}
|
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
import { stringify, toolInput, toolResult, toolResultText } from "../shared/parts.ts"
|
|
2
|
-
import type { ContentPartLike, MessageLike } from "./select.ts"
|
|
3
|
-
|
|
4
|
-
/** 单条工具输出最多保留的字符数。 */
|
|
5
|
-
const TOOL_RESULT_MAX = 2_000
|
|
6
|
-
/** 工具调用参数最多保留的字符数。 */
|
|
7
|
-
const TOOL_INPUT_MAX = 500
|
|
8
|
-
/** 整份 transcript 的字符上限,防止侧调用自身溢出。 */
|
|
9
|
-
export const TRANSCRIPT_MAX = 200_000
|
|
10
|
-
|
|
11
|
-
function truncate(value: string, max: number): string {
|
|
12
|
-
return value.length <= max ? value : `${value.slice(0, max)}…[truncated]`
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function renderMessage(message: MessageLike): string {
|
|
16
|
-
const lines: string[] = []
|
|
17
|
-
if (typeof message.text === "string" && message.text.length > 0) {
|
|
18
|
-
lines.push(message.role === "user" ? `[User]\n${message.text}` : message.text)
|
|
19
|
-
}
|
|
20
|
-
const content: readonly ContentPartLike[] = Array.isArray(message.content)
|
|
21
|
-
? message.content
|
|
22
|
-
: []
|
|
23
|
-
for (const part of content) {
|
|
24
|
-
if (part.type === "text") {
|
|
25
|
-
const text = part.text ?? ""
|
|
26
|
-
if (text.length > 0) lines.push(message.role === "user" ? `[User]\n${text}` : text)
|
|
27
|
-
continue
|
|
28
|
-
}
|
|
29
|
-
if (part.type === "tool") {
|
|
30
|
-
lines.push(`[Tool call ${part.name ?? "?"}] ${truncate(stringify(toolInput(part)), TOOL_INPUT_MAX)}`)
|
|
31
|
-
lines.push(`[Tool result ${part.name ?? "?"}] ${truncate(toolResultText(toolResult(part)), TOOL_RESULT_MAX)}`)
|
|
32
|
-
continue
|
|
33
|
-
}
|
|
34
|
-
if (part.type === "tool-call") {
|
|
35
|
-
lines.push(`[Tool call ${part.name ?? "?"}] ${truncate(stringify(toolInput(part)), TOOL_INPUT_MAX)}`)
|
|
36
|
-
continue
|
|
37
|
-
}
|
|
38
|
-
if (part.type === "tool-result") {
|
|
39
|
-
lines.push(`[Tool result ${part.name ?? "?"}] ${truncate(toolResultText(toolResult(part)), TOOL_RESULT_MAX)}`)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return lines.join("\n")
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* 输入:可压缩消息。
|
|
47
|
-
* 输出:给侧调用使用的纯文本 transcript。
|
|
48
|
-
* 预期:跳过 reasoning/media;工具输出截断;整体超限时丢弃最老的消息。
|
|
49
|
-
*/
|
|
50
|
-
export function buildTranscript(messages: readonly MessageLike[]): string {
|
|
51
|
-
const rendered = messages.map(renderMessage).filter((text) => text.length > 0)
|
|
52
|
-
let total = rendered.reduce((sum, text) => sum + text.length + 2, 0)
|
|
53
|
-
let start = 0
|
|
54
|
-
while (total > TRANSCRIPT_MAX && start < rendered.length - 1) {
|
|
55
|
-
const item = rendered[start]
|
|
56
|
-
if (item === undefined) break
|
|
57
|
-
total -= item.length + 2
|
|
58
|
-
start += 1
|
|
59
|
-
}
|
|
60
|
-
const kept = rendered.slice(start)
|
|
61
|
-
const prefix = start > 0 ? `[older messages omitted: ${start.toString()}]\n\n` : ""
|
|
62
|
-
return prefix + kept.join("\n\n")
|
|
63
|
-
}
|
|
1
|
+
import { stringify, toolInput, toolResult, toolResultText } from "../shared/parts.ts"
|
|
2
|
+
import type { ContentPartLike, MessageLike } from "./select.ts"
|
|
3
|
+
|
|
4
|
+
/** 单条工具输出最多保留的字符数。 */
|
|
5
|
+
const TOOL_RESULT_MAX = 2_000
|
|
6
|
+
/** 工具调用参数最多保留的字符数。 */
|
|
7
|
+
const TOOL_INPUT_MAX = 500
|
|
8
|
+
/** 整份 transcript 的字符上限,防止侧调用自身溢出。 */
|
|
9
|
+
export const TRANSCRIPT_MAX = 200_000
|
|
10
|
+
|
|
11
|
+
function truncate(value: string, max: number): string {
|
|
12
|
+
return value.length <= max ? value : `${value.slice(0, max)}…[truncated]`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderMessage(message: MessageLike): string {
|
|
16
|
+
const lines: string[] = []
|
|
17
|
+
if (typeof message.text === "string" && message.text.length > 0) {
|
|
18
|
+
lines.push(message.role === "user" ? `[User]\n${message.text}` : message.text)
|
|
19
|
+
}
|
|
20
|
+
const content: readonly ContentPartLike[] = Array.isArray(message.content)
|
|
21
|
+
? message.content
|
|
22
|
+
: []
|
|
23
|
+
for (const part of content) {
|
|
24
|
+
if (part.type === "text") {
|
|
25
|
+
const text = part.text ?? ""
|
|
26
|
+
if (text.length > 0) lines.push(message.role === "user" ? `[User]\n${text}` : text)
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
if (part.type === "tool") {
|
|
30
|
+
lines.push(`[Tool call ${part.name ?? "?"}] ${truncate(stringify(toolInput(part)), TOOL_INPUT_MAX)}`)
|
|
31
|
+
lines.push(`[Tool result ${part.name ?? "?"}] ${truncate(toolResultText(toolResult(part)), TOOL_RESULT_MAX)}`)
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
if (part.type === "tool-call") {
|
|
35
|
+
lines.push(`[Tool call ${part.name ?? "?"}] ${truncate(stringify(toolInput(part)), TOOL_INPUT_MAX)}`)
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
if (part.type === "tool-result") {
|
|
39
|
+
lines.push(`[Tool result ${part.name ?? "?"}] ${truncate(toolResultText(toolResult(part)), TOOL_RESULT_MAX)}`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return lines.join("\n")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 输入:可压缩消息。
|
|
47
|
+
* 输出:给侧调用使用的纯文本 transcript。
|
|
48
|
+
* 预期:跳过 reasoning/media;工具输出截断;整体超限时丢弃最老的消息。
|
|
49
|
+
*/
|
|
50
|
+
export function buildTranscript(messages: readonly MessageLike[]): string {
|
|
51
|
+
const rendered = messages.map(renderMessage).filter((text) => text.length > 0)
|
|
52
|
+
let total = rendered.reduce((sum, text) => sum + text.length + 2, 0)
|
|
53
|
+
let start = 0
|
|
54
|
+
while (total > TRANSCRIPT_MAX && start < rendered.length - 1) {
|
|
55
|
+
const item = rendered[start]
|
|
56
|
+
if (item === undefined) break
|
|
57
|
+
total -= item.length + 2
|
|
58
|
+
start += 1
|
|
59
|
+
}
|
|
60
|
+
const kept = rendered.slice(start)
|
|
61
|
+
const prefix = start > 0 ? `[older messages omitted: ${start.toString()}]\n\n` : ""
|
|
62
|
+
return prefix + kept.join("\n\n")
|
|
63
|
+
}
|