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,123 +1,123 @@
|
|
|
1
|
-
import { stringify, toolInput, toolResult, toolResultText } from "../shared/parts.ts"
|
|
2
|
-
|
|
3
|
-
/** 媒体 source 的最小结构;bytes/base64 可落盘,url/ref 只保留引用。 */
|
|
4
|
-
export interface MediaSourceLike {
|
|
5
|
-
readonly type: string
|
|
6
|
-
readonly data?: string | Uint8Array | undefined
|
|
7
|
-
readonly mediaType?: string | undefined
|
|
8
|
-
readonly url?: string | undefined
|
|
9
|
-
readonly provider?: string | undefined
|
|
10
|
-
readonly id?: string | undefined
|
|
11
|
-
readonly expiresAt?: number | undefined
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/** 单条内容 part 的最小结构,避免绑定 @opencode/ai 的具体类。 */
|
|
15
|
-
export interface ContentPartLike {
|
|
16
|
-
readonly type: string
|
|
17
|
-
readonly text?: string | null | undefined
|
|
18
|
-
readonly name?: string | undefined
|
|
19
|
-
readonly input?: unknown
|
|
20
|
-
readonly result?: unknown
|
|
21
|
-
readonly state?:
|
|
22
|
-
| {
|
|
23
|
-
readonly input?: unknown
|
|
24
|
-
readonly content?: unknown
|
|
25
|
-
readonly output?: unknown
|
|
26
|
-
}
|
|
27
|
-
| undefined
|
|
28
|
-
readonly filename?: string | undefined
|
|
29
|
-
readonly media?:
|
|
30
|
-
| {
|
|
31
|
-
readonly source?: MediaSourceLike | undefined
|
|
32
|
-
readonly mediaType?: string | undefined
|
|
33
|
-
readonly info?:
|
|
34
|
-
| {
|
|
35
|
-
readonly width?: number | undefined
|
|
36
|
-
readonly height?: number | undefined
|
|
37
|
-
readonly durationSeconds?: number | undefined
|
|
38
|
-
}
|
|
39
|
-
| undefined
|
|
40
|
-
readonly providerMetadata?: unknown
|
|
41
|
-
}
|
|
42
|
-
| undefined
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** 单条消息的最小结构。 */
|
|
46
|
-
export interface MessageLike {
|
|
47
|
-
readonly id?: string | undefined
|
|
48
|
-
readonly role: string
|
|
49
|
-
readonly content: readonly ContentPartLike[]
|
|
50
|
-
/** v2 用户消息把正文放在 text,而不是 content 数组。 */
|
|
51
|
-
readonly text?: string | null | undefined
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export const CHECKPOINT_PREFIX = "<conversation-checkpoint>"
|
|
55
|
-
|
|
56
|
-
/** 输入:消息;输出:纯文本(只拼 text part)。 */
|
|
57
|
-
export function messageText(message: MessageLike): string {
|
|
58
|
-
const direct = typeof message.text === "string" ? message.text : ""
|
|
59
|
-
const content: readonly ContentPartLike[] = Array.isArray(message.content)
|
|
60
|
-
? message.content
|
|
61
|
-
: []
|
|
62
|
-
const parts = content
|
|
63
|
-
.filter((part) => part.type === "text")
|
|
64
|
-
.map((part) => part.text ?? "")
|
|
65
|
-
.join("\n")
|
|
66
|
-
return [direct, parts].filter((text) => text.length > 0).join("\n")
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** 输入:消息;输出:是否是 checkpoint 消息。 */
|
|
70
|
-
export function isCheckpoint(message: MessageLike): boolean {
|
|
71
|
-
return messageText(message).trimStart().startsWith(CHECKPOINT_PREFIX)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* 输入:当前请求的消息列表。
|
|
76
|
-
* 输出:可压缩的消息(最后一条 user 之前,排除 checkpoint)。
|
|
77
|
-
* 预期:没有 user 消息时返回空数组。
|
|
78
|
-
*/
|
|
79
|
-
export function eligibleMessages(messages: readonly MessageLike[]): readonly MessageLike[] {
|
|
80
|
-
const lastUser = messages.findLastIndex((message) => message.role === "user")
|
|
81
|
-
if (lastUser <= 0) return []
|
|
82
|
-
return messages.slice(0, lastUser).filter((message) => !isCheckpoint(message))
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* 输入:compaction 钩子的 older 消息。
|
|
87
|
-
* 输出:可压缩消息(排除 checkpoint)。
|
|
88
|
-
* 预期:older 里没有“当前 user”可排除,不能复用 eligibleMessages。
|
|
89
|
-
*/
|
|
90
|
-
export function olderMessages(messages: readonly MessageLike[]): readonly MessageLike[] {
|
|
91
|
-
return messages.filter((message) => !isCheckpoint(message))
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** 输入:消息列表;输出:首尾带 id 的消息 id;没有可用 id 时返回 undefined。 */
|
|
95
|
-
export function messageRange(
|
|
96
|
-
messages: readonly MessageLike[],
|
|
97
|
-
): { readonly fromId: string; readonly toId: string } | undefined {
|
|
98
|
-
const first = messages.find((message) => message.id !== undefined)
|
|
99
|
-
const last = [...messages].reverse().find((message) => message.id !== undefined)
|
|
100
|
-
if (first?.id === undefined || last?.id === undefined) return undefined
|
|
101
|
-
return { fromId: first.id, toId: last.id }
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** 输入:消息列表;输出:所有 text part 的总字符数。 */
|
|
105
|
-
export function textChars(messages: readonly MessageLike[]): number {
|
|
106
|
-
let total = 0
|
|
107
|
-
for (const message of messages) {
|
|
108
|
-
if (typeof message.text === "string") total += message.text.length
|
|
109
|
-
const content: readonly ContentPartLike[] = Array.isArray(message.content)
|
|
110
|
-
? message.content
|
|
111
|
-
: []
|
|
112
|
-
for (const part of content) {
|
|
113
|
-
if (part.type === "text") {
|
|
114
|
-
total += (part.text ?? "").length
|
|
115
|
-
continue
|
|
116
|
-
}
|
|
117
|
-
if (part.type === "tool" || part.type === "tool-call" || part.type === "tool-result") {
|
|
118
|
-
total += `${stringify(toolInput(part))}${toolResultText(toolResult(part))}`.length
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return total
|
|
123
|
-
}
|
|
1
|
+
import { stringify, toolInput, toolResult, toolResultText } from "../shared/parts.ts"
|
|
2
|
+
|
|
3
|
+
/** 媒体 source 的最小结构;bytes/base64 可落盘,url/ref 只保留引用。 */
|
|
4
|
+
export interface MediaSourceLike {
|
|
5
|
+
readonly type: string
|
|
6
|
+
readonly data?: string | Uint8Array | undefined
|
|
7
|
+
readonly mediaType?: string | undefined
|
|
8
|
+
readonly url?: string | undefined
|
|
9
|
+
readonly provider?: string | undefined
|
|
10
|
+
readonly id?: string | undefined
|
|
11
|
+
readonly expiresAt?: number | undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 单条内容 part 的最小结构,避免绑定 @opencode/ai 的具体类。 */
|
|
15
|
+
export interface ContentPartLike {
|
|
16
|
+
readonly type: string
|
|
17
|
+
readonly text?: string | null | undefined
|
|
18
|
+
readonly name?: string | undefined
|
|
19
|
+
readonly input?: unknown
|
|
20
|
+
readonly result?: unknown
|
|
21
|
+
readonly state?:
|
|
22
|
+
| {
|
|
23
|
+
readonly input?: unknown
|
|
24
|
+
readonly content?: unknown
|
|
25
|
+
readonly output?: unknown
|
|
26
|
+
}
|
|
27
|
+
| undefined
|
|
28
|
+
readonly filename?: string | undefined
|
|
29
|
+
readonly media?:
|
|
30
|
+
| {
|
|
31
|
+
readonly source?: MediaSourceLike | undefined
|
|
32
|
+
readonly mediaType?: string | undefined
|
|
33
|
+
readonly info?:
|
|
34
|
+
| {
|
|
35
|
+
readonly width?: number | undefined
|
|
36
|
+
readonly height?: number | undefined
|
|
37
|
+
readonly durationSeconds?: number | undefined
|
|
38
|
+
}
|
|
39
|
+
| undefined
|
|
40
|
+
readonly providerMetadata?: unknown
|
|
41
|
+
}
|
|
42
|
+
| undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 单条消息的最小结构。 */
|
|
46
|
+
export interface MessageLike {
|
|
47
|
+
readonly id?: string | undefined
|
|
48
|
+
readonly role: string
|
|
49
|
+
readonly content: readonly ContentPartLike[]
|
|
50
|
+
/** v2 用户消息把正文放在 text,而不是 content 数组。 */
|
|
51
|
+
readonly text?: string | null | undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const CHECKPOINT_PREFIX = "<conversation-checkpoint>"
|
|
55
|
+
|
|
56
|
+
/** 输入:消息;输出:纯文本(只拼 text part)。 */
|
|
57
|
+
export function messageText(message: MessageLike): string {
|
|
58
|
+
const direct = typeof message.text === "string" ? message.text : ""
|
|
59
|
+
const content: readonly ContentPartLike[] = Array.isArray(message.content)
|
|
60
|
+
? message.content
|
|
61
|
+
: []
|
|
62
|
+
const parts = content
|
|
63
|
+
.filter((part) => part.type === "text")
|
|
64
|
+
.map((part) => part.text ?? "")
|
|
65
|
+
.join("\n")
|
|
66
|
+
return [direct, parts].filter((text) => text.length > 0).join("\n")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 输入:消息;输出:是否是 checkpoint 消息。 */
|
|
70
|
+
export function isCheckpoint(message: MessageLike): boolean {
|
|
71
|
+
return messageText(message).trimStart().startsWith(CHECKPOINT_PREFIX)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 输入:当前请求的消息列表。
|
|
76
|
+
* 输出:可压缩的消息(最后一条 user 之前,排除 checkpoint)。
|
|
77
|
+
* 预期:没有 user 消息时返回空数组。
|
|
78
|
+
*/
|
|
79
|
+
export function eligibleMessages(messages: readonly MessageLike[]): readonly MessageLike[] {
|
|
80
|
+
const lastUser = messages.findLastIndex((message) => message.role === "user")
|
|
81
|
+
if (lastUser <= 0) return []
|
|
82
|
+
return messages.slice(0, lastUser).filter((message) => !isCheckpoint(message))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 输入:compaction 钩子的 older 消息。
|
|
87
|
+
* 输出:可压缩消息(排除 checkpoint)。
|
|
88
|
+
* 预期:older 里没有“当前 user”可排除,不能复用 eligibleMessages。
|
|
89
|
+
*/
|
|
90
|
+
export function olderMessages(messages: readonly MessageLike[]): readonly MessageLike[] {
|
|
91
|
+
return messages.filter((message) => !isCheckpoint(message))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 输入:消息列表;输出:首尾带 id 的消息 id;没有可用 id 时返回 undefined。 */
|
|
95
|
+
export function messageRange(
|
|
96
|
+
messages: readonly MessageLike[],
|
|
97
|
+
): { readonly fromId: string; readonly toId: string } | undefined {
|
|
98
|
+
const first = messages.find((message) => message.id !== undefined)
|
|
99
|
+
const last = [...messages].reverse().find((message) => message.id !== undefined)
|
|
100
|
+
if (first?.id === undefined || last?.id === undefined) return undefined
|
|
101
|
+
return { fromId: first.id, toId: last.id }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 输入:消息列表;输出:所有 text part 的总字符数。 */
|
|
105
|
+
export function textChars(messages: readonly MessageLike[]): number {
|
|
106
|
+
let total = 0
|
|
107
|
+
for (const message of messages) {
|
|
108
|
+
if (typeof message.text === "string") total += message.text.length
|
|
109
|
+
const content: readonly ContentPartLike[] = Array.isArray(message.content)
|
|
110
|
+
? message.content
|
|
111
|
+
: []
|
|
112
|
+
for (const part of content) {
|
|
113
|
+
if (part.type === "text") {
|
|
114
|
+
total += (part.text ?? "").length
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
if (part.type === "tool" || part.type === "tool-call" || part.type === "tool-result") {
|
|
118
|
+
total += `${stringify(toolInput(part))}${toolResultText(toolResult(part))}`.length
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return total
|
|
123
|
+
}
|
|
@@ -1,112 +1,112 @@
|
|
|
1
|
-
import type { ContentPartLike, MediaSourceLike, MessageLike } from "./select.ts"
|
|
2
|
-
|
|
3
|
-
/** 输入:任意值;输出:对象记录或 undefined。 */
|
|
4
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
5
|
-
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/** 输入:user.files 项;输出:media part;形状不合法时 undefined。 */
|
|
9
|
-
function userFileToPart(value: unknown): ContentPartLike | undefined {
|
|
10
|
-
const file = asRecord(value)
|
|
11
|
-
if (file === undefined || typeof file["mime"] !== "string") return undefined
|
|
12
|
-
const source = asRecord(file["source"])
|
|
13
|
-
const data = typeof file["data"] === "string" ? file["data"] : undefined
|
|
14
|
-
const uri = typeof source?.["uri"] === "string" ? source["uri"] : undefined
|
|
15
|
-
const sourceKind = typeof source?.["type"] === "string" ? source["type"] : "inline"
|
|
16
|
-
const media: MediaSourceLike =
|
|
17
|
-
sourceKind === "uri" && uri !== undefined
|
|
18
|
-
? { type: "url", url: uri, mediaType: file["mime"] }
|
|
19
|
-
: { type: "base64", ...(data === undefined ? {} : { data }), mediaType: file["mime"] }
|
|
20
|
-
return {
|
|
21
|
-
type: "media",
|
|
22
|
-
...(typeof file["name"] === "string" ? { filename: file["name"] } : {}),
|
|
23
|
-
media: { source: media },
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** 输入:assistant.content 项;输出:可读 parts;reasoning 跳过。 */
|
|
28
|
-
function assistantPartToParts(value: unknown): readonly ContentPartLike[] {
|
|
29
|
-
const part = asRecord(value)
|
|
30
|
-
if (part === undefined) return []
|
|
31
|
-
if (part["type"] === "text" && typeof part["text"] === "string") {
|
|
32
|
-
return [{ type: "text", text: part["text"] }]
|
|
33
|
-
}
|
|
34
|
-
if (part["type"] === "tool" && typeof part["name"] === "string") {
|
|
35
|
-
const state = asRecord(part["state"])
|
|
36
|
-
const parts: ContentPartLike[] = [
|
|
37
|
-
{
|
|
38
|
-
type: "tool-call",
|
|
39
|
-
name: part["name"],
|
|
40
|
-
input: state?.["input"],
|
|
41
|
-
},
|
|
42
|
-
]
|
|
43
|
-
if (state?.["status"] === "completed") {
|
|
44
|
-
parts.push({
|
|
45
|
-
type: "tool-result",
|
|
46
|
-
name: part["name"],
|
|
47
|
-
result: state["content"] ?? state["input"],
|
|
48
|
-
})
|
|
49
|
-
}
|
|
50
|
-
return parts
|
|
51
|
-
}
|
|
52
|
-
if (part["type"] === "media") {
|
|
53
|
-
const media = asRecord(part["media"])
|
|
54
|
-
const source = asRecord(media?.["source"])
|
|
55
|
-
if (source === undefined || typeof source["type"] !== "string") return []
|
|
56
|
-
return [
|
|
57
|
-
{
|
|
58
|
-
type: "media",
|
|
59
|
-
...(typeof part["filename"] === "string" ? { filename: part["filename"] } : {}),
|
|
60
|
-
media: {
|
|
61
|
-
source: {
|
|
62
|
-
type: source["type"],
|
|
63
|
-
...(typeof source["data"] === "string" ? { data: source["data"] } : {}),
|
|
64
|
-
...(source["data"] instanceof Uint8Array ? { data: source["data"] } : {}),
|
|
65
|
-
...(typeof source["mediaType"] === "string" ? { mediaType: source["mediaType"] } : {}),
|
|
66
|
-
...(typeof source["url"] === "string" ? { url: source["url"] } : {}),
|
|
67
|
-
...(typeof source["provider"] === "string" ? { provider: source["provider"] } : {}),
|
|
68
|
-
...(typeof source["id"] === "string" ? { id: source["id"] } : {}),
|
|
69
|
-
...(typeof source["expiresAt"] === "number" ? { expiresAt: source["expiresAt"] } : {}),
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
]
|
|
74
|
-
}
|
|
75
|
-
return []
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* 输入:ctx.session.context 返回的持久化 session 消息。
|
|
80
|
-
* 输出:MessageLike 列表;user.text 转 text,user.files 转 media,assistant.content 转 parts。
|
|
81
|
-
* 预期:agent/model/compaction 等非对话项跳过;reasoning 不进入压缩 transcript。
|
|
82
|
-
*/
|
|
83
|
-
export function sessionMessagesToMessageLikes(messages: readonly unknown[]): readonly MessageLike[] {
|
|
84
|
-
const out: MessageLike[] = []
|
|
85
|
-
for (const item of messages) {
|
|
86
|
-
const record = asRecord(item)
|
|
87
|
-
if (record === undefined) continue
|
|
88
|
-
const id = typeof record["id"] === "string" ? record["id"] : undefined
|
|
89
|
-
if (record["type"] === "user") {
|
|
90
|
-
const content: ContentPartLike[] = []
|
|
91
|
-
if (typeof record["text"] === "string" && record["text"].length > 0) {
|
|
92
|
-
content.push({ type: "text", text: record["text"] })
|
|
93
|
-
}
|
|
94
|
-
if (Array.isArray(record["files"])) {
|
|
95
|
-
for (const file of record["files"]) {
|
|
96
|
-
const part = userFileToPart(file)
|
|
97
|
-
if (part !== undefined) content.push(part)
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
out.push({ ...(id === undefined ? {} : { id }), role: "user", content })
|
|
101
|
-
continue
|
|
102
|
-
}
|
|
103
|
-
if (record["type"] === "assistant" && Array.isArray(record["content"])) {
|
|
104
|
-
const content: ContentPartLike[] = []
|
|
105
|
-
for (const part of record["content"]) {
|
|
106
|
-
content.push(...assistantPartToParts(part))
|
|
107
|
-
}
|
|
108
|
-
out.push({ ...(id === undefined ? {} : { id }), role: "assistant", content })
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return out
|
|
112
|
-
}
|
|
1
|
+
import type { ContentPartLike, MediaSourceLike, MessageLike } from "./select.ts"
|
|
2
|
+
|
|
3
|
+
/** 输入:任意值;输出:对象记录或 undefined。 */
|
|
4
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
5
|
+
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : undefined
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** 输入:user.files 项;输出:media part;形状不合法时 undefined。 */
|
|
9
|
+
function userFileToPart(value: unknown): ContentPartLike | undefined {
|
|
10
|
+
const file = asRecord(value)
|
|
11
|
+
if (file === undefined || typeof file["mime"] !== "string") return undefined
|
|
12
|
+
const source = asRecord(file["source"])
|
|
13
|
+
const data = typeof file["data"] === "string" ? file["data"] : undefined
|
|
14
|
+
const uri = typeof source?.["uri"] === "string" ? source["uri"] : undefined
|
|
15
|
+
const sourceKind = typeof source?.["type"] === "string" ? source["type"] : "inline"
|
|
16
|
+
const media: MediaSourceLike =
|
|
17
|
+
sourceKind === "uri" && uri !== undefined
|
|
18
|
+
? { type: "url", url: uri, mediaType: file["mime"] }
|
|
19
|
+
: { type: "base64", ...(data === undefined ? {} : { data }), mediaType: file["mime"] }
|
|
20
|
+
return {
|
|
21
|
+
type: "media",
|
|
22
|
+
...(typeof file["name"] === "string" ? { filename: file["name"] } : {}),
|
|
23
|
+
media: { source: media },
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 输入:assistant.content 项;输出:可读 parts;reasoning 跳过。 */
|
|
28
|
+
function assistantPartToParts(value: unknown): readonly ContentPartLike[] {
|
|
29
|
+
const part = asRecord(value)
|
|
30
|
+
if (part === undefined) return []
|
|
31
|
+
if (part["type"] === "text" && typeof part["text"] === "string") {
|
|
32
|
+
return [{ type: "text", text: part["text"] }]
|
|
33
|
+
}
|
|
34
|
+
if (part["type"] === "tool" && typeof part["name"] === "string") {
|
|
35
|
+
const state = asRecord(part["state"])
|
|
36
|
+
const parts: ContentPartLike[] = [
|
|
37
|
+
{
|
|
38
|
+
type: "tool-call",
|
|
39
|
+
name: part["name"],
|
|
40
|
+
input: state?.["input"],
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
if (state?.["status"] === "completed") {
|
|
44
|
+
parts.push({
|
|
45
|
+
type: "tool-result",
|
|
46
|
+
name: part["name"],
|
|
47
|
+
result: state["content"] ?? state["input"],
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
return parts
|
|
51
|
+
}
|
|
52
|
+
if (part["type"] === "media") {
|
|
53
|
+
const media = asRecord(part["media"])
|
|
54
|
+
const source = asRecord(media?.["source"])
|
|
55
|
+
if (source === undefined || typeof source["type"] !== "string") return []
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
type: "media",
|
|
59
|
+
...(typeof part["filename"] === "string" ? { filename: part["filename"] } : {}),
|
|
60
|
+
media: {
|
|
61
|
+
source: {
|
|
62
|
+
type: source["type"],
|
|
63
|
+
...(typeof source["data"] === "string" ? { data: source["data"] } : {}),
|
|
64
|
+
...(source["data"] instanceof Uint8Array ? { data: source["data"] } : {}),
|
|
65
|
+
...(typeof source["mediaType"] === "string" ? { mediaType: source["mediaType"] } : {}),
|
|
66
|
+
...(typeof source["url"] === "string" ? { url: source["url"] } : {}),
|
|
67
|
+
...(typeof source["provider"] === "string" ? { provider: source["provider"] } : {}),
|
|
68
|
+
...(typeof source["id"] === "string" ? { id: source["id"] } : {}),
|
|
69
|
+
...(typeof source["expiresAt"] === "number" ? { expiresAt: source["expiresAt"] } : {}),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
return []
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 输入:ctx.session.context 返回的持久化 session 消息。
|
|
80
|
+
* 输出:MessageLike 列表;user.text 转 text,user.files 转 media,assistant.content 转 parts。
|
|
81
|
+
* 预期:agent/model/compaction 等非对话项跳过;reasoning 不进入压缩 transcript。
|
|
82
|
+
*/
|
|
83
|
+
export function sessionMessagesToMessageLikes(messages: readonly unknown[]): readonly MessageLike[] {
|
|
84
|
+
const out: MessageLike[] = []
|
|
85
|
+
for (const item of messages) {
|
|
86
|
+
const record = asRecord(item)
|
|
87
|
+
if (record === undefined) continue
|
|
88
|
+
const id = typeof record["id"] === "string" ? record["id"] : undefined
|
|
89
|
+
if (record["type"] === "user") {
|
|
90
|
+
const content: ContentPartLike[] = []
|
|
91
|
+
if (typeof record["text"] === "string" && record["text"].length > 0) {
|
|
92
|
+
content.push({ type: "text", text: record["text"] })
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(record["files"])) {
|
|
95
|
+
for (const file of record["files"]) {
|
|
96
|
+
const part = userFileToPart(file)
|
|
97
|
+
if (part !== undefined) content.push(part)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
out.push({ ...(id === undefined ? {} : { id }), role: "user", content })
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
if (record["type"] === "assistant" && Array.isArray(record["content"])) {
|
|
104
|
+
const content: ContentPartLike[] = []
|
|
105
|
+
for (const part of record["content"]) {
|
|
106
|
+
content.push(...assistantPartToParts(part))
|
|
107
|
+
}
|
|
108
|
+
out.push({ ...(id === undefined ? {} : { id }), role: "assistant", content })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out
|
|
112
|
+
}
|