@downcity/agent 1.1.166 → 1.1.170
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/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +2 -0
- package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts +1 -0
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +2 -1
- package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
- package/bin/executor/messages/SessionAttachmentMapper.d.ts +10 -0
- package/bin/executor/messages/SessionAttachmentMapper.d.ts.map +1 -1
- package/bin/executor/messages/SessionAttachmentMapper.js +49 -0
- package/bin/executor/messages/SessionAttachmentMapper.js.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +2 -1
- package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
- package/bin/executor/types/SessionMessages.d.ts +2 -0
- package/bin/executor/types/SessionMessages.d.ts.map +1 -1
- package/bin/session/browse/Browse.d.ts +8 -0
- package/bin/session/browse/Browse.d.ts.map +1 -1
- package/bin/session/browse/Browse.js +58 -6
- package/bin/session/browse/Browse.js.map +1 -1
- package/bin/session/index.d.ts +2 -2
- package/bin/session/index.d.ts.map +1 -1
- package/bin/session/index.js +2 -2
- package/bin/session/index.js.map +1 -1
- package/bin/session/services/SessionStateService.d.ts.map +1 -1
- package/bin/session/services/SessionStateService.js +3 -2
- package/bin/session/services/SessionStateService.js.map +1 -1
- package/bin/session/services/SessionViewService.d.ts.map +1 -1
- package/bin/session/services/SessionViewService.js +8 -4
- package/bin/session/services/SessionViewService.js.map +1 -1
- package/bin/session/storage/Paths.d.ts +8 -0
- package/bin/session/storage/Paths.d.ts.map +1 -1
- package/bin/session/storage/Paths.js +10 -0
- package/bin/session/storage/Paths.js.map +1 -1
- package/bin/types/agent/SessionTypes.d.ts +26 -2
- package/bin/types/agent/SessionTypes.d.ts.map +1 -1
- package/package.json +3 -3
- package/scripts/assistant-file-resource.test.mjs +59 -0
- package/scripts/session-history-archive.test.mjs +188 -0
- package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +1 -0
- package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +3 -2
- package/src/executor/messages/SessionAttachmentMapper.ts +61 -0
- package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +2 -1
- package/src/executor/types/SessionMessages.ts +2 -0
- package/src/session/browse/Browse.ts +72 -6
- package/src/session/index.ts +2 -0
- package/src/session/services/SessionStateService.ts +6 -2
- package/src/session/services/SessionViewService.ts +16 -3
- package/src/session/storage/Paths.ts +19 -0
- package/src/types/agent/SessionTypes.ts +26 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 验证 session.history 的 compact archive 分层读取语义。
|
|
3
|
+
*
|
|
4
|
+
* 关键点(中文)
|
|
5
|
+
* - compact summary 是模型上下文消息,但不应作为用户可见 history item 返回。
|
|
6
|
+
* - 调用方通过 previous_archive_id 再传入 archive_id,读取上一层被覆盖历史。
|
|
7
|
+
* - history 分页字段使用 snake_case,保持 SDK 对外数据命名一致。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import fs from "node:fs/promises";
|
|
15
|
+
|
|
16
|
+
import { Agent } from "../bin/index.js";
|
|
17
|
+
|
|
18
|
+
function user_message(session_id, index) {
|
|
19
|
+
return {
|
|
20
|
+
id: `u:${index}`,
|
|
21
|
+
role: "user",
|
|
22
|
+
metadata: {
|
|
23
|
+
v: 1,
|
|
24
|
+
ts: index,
|
|
25
|
+
sessionId: session_id,
|
|
26
|
+
source: "ingress",
|
|
27
|
+
},
|
|
28
|
+
parts: [{ type: "text", text: `user ${index}` }],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assistant_message(session_id, index) {
|
|
33
|
+
return {
|
|
34
|
+
id: `a:${index}`,
|
|
35
|
+
role: "assistant",
|
|
36
|
+
metadata: {
|
|
37
|
+
v: 1,
|
|
38
|
+
ts: index,
|
|
39
|
+
sessionId: session_id,
|
|
40
|
+
source: "egress",
|
|
41
|
+
},
|
|
42
|
+
parts: [{ type: "text", text: `assistant ${index}` }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function compact_summary(session_id, archive_id, text) {
|
|
47
|
+
return {
|
|
48
|
+
id: `summary:${archive_id}`,
|
|
49
|
+
role: "assistant",
|
|
50
|
+
metadata: {
|
|
51
|
+
v: 1,
|
|
52
|
+
ts: 1000,
|
|
53
|
+
sessionId: session_id,
|
|
54
|
+
source: "compact",
|
|
55
|
+
kind: "summary",
|
|
56
|
+
archiveId: archive_id,
|
|
57
|
+
sourceRange: {
|
|
58
|
+
fromId: "start",
|
|
59
|
+
toId: "end",
|
|
60
|
+
count: 2,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
parts: [{ type: "text", text }],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function write_jsonl(file_path, messages) {
|
|
68
|
+
await fs.mkdir(path.dirname(file_path), { recursive: true });
|
|
69
|
+
await fs.writeFile(
|
|
70
|
+
file_path,
|
|
71
|
+
messages.map((message) => JSON.stringify(message)).join("\n") + "\n",
|
|
72
|
+
"utf8",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function write_archive(file_path, session_id, messages) {
|
|
77
|
+
await fs.mkdir(path.dirname(file_path), { recursive: true });
|
|
78
|
+
await fs.writeFile(
|
|
79
|
+
file_path,
|
|
80
|
+
JSON.stringify(
|
|
81
|
+
{
|
|
82
|
+
v: 1,
|
|
83
|
+
sessionId: session_id,
|
|
84
|
+
archivedAt: Date.now(),
|
|
85
|
+
messages,
|
|
86
|
+
},
|
|
87
|
+
null,
|
|
88
|
+
2,
|
|
89
|
+
),
|
|
90
|
+
"utf8",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
test("session.history hides compact summary and reads archive layers by archive_id", async () => {
|
|
95
|
+
const agent_path = await fs.mkdtemp(
|
|
96
|
+
path.join(os.tmpdir(), "downcity-agent-history-archive-"),
|
|
97
|
+
);
|
|
98
|
+
const agent = new Agent({
|
|
99
|
+
id: "history_archive_agent",
|
|
100
|
+
path: agent_path,
|
|
101
|
+
});
|
|
102
|
+
const session = await agent.session_collection().create_session({
|
|
103
|
+
sessionId: "history_archive_session",
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const messages_dir = path.join(
|
|
107
|
+
agent_path,
|
|
108
|
+
".downcity",
|
|
109
|
+
"agents",
|
|
110
|
+
encodeURIComponent("history_archive_agent"),
|
|
111
|
+
"sessions",
|
|
112
|
+
encodeURIComponent(session.id),
|
|
113
|
+
"messages",
|
|
114
|
+
);
|
|
115
|
+
const messages_path = path.join(messages_dir, "messages.jsonl");
|
|
116
|
+
const archive_dir = path.join(messages_dir, "archive");
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await write_jsonl(messages_path, [
|
|
120
|
+
compact_summary(session.id, "compact-B", "summary B"),
|
|
121
|
+
user_message(session.id, 31),
|
|
122
|
+
assistant_message(session.id, 32),
|
|
123
|
+
]);
|
|
124
|
+
await write_archive(path.join(archive_dir, "compact-B.json"), session.id, [
|
|
125
|
+
compact_summary(session.id, "compact-A", "summary A"),
|
|
126
|
+
user_message(session.id, 16),
|
|
127
|
+
assistant_message(session.id, 17),
|
|
128
|
+
]);
|
|
129
|
+
await write_archive(path.join(archive_dir, "compact-A.json"), session.id, [
|
|
130
|
+
user_message(session.id, 1),
|
|
131
|
+
assistant_message(session.id, 2),
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
const current_page = await session.history({
|
|
135
|
+
limit: 1,
|
|
136
|
+
});
|
|
137
|
+
assert.equal(current_page.total, 2);
|
|
138
|
+
assert.equal(current_page.has_more, true);
|
|
139
|
+
assert.equal(current_page.next_cursor, "1");
|
|
140
|
+
assert.equal(current_page.archive_id, undefined);
|
|
141
|
+
assert.equal(current_page.previous_archive_id, "compact-B");
|
|
142
|
+
assert.deepEqual(
|
|
143
|
+
current_page.items.map((message) => message.id),
|
|
144
|
+
["u:31"],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const next_current_page = await session.history({
|
|
148
|
+
cursor: current_page.next_cursor,
|
|
149
|
+
});
|
|
150
|
+
assert.equal(next_current_page.has_more, false);
|
|
151
|
+
assert.deepEqual(
|
|
152
|
+
next_current_page.items.map((message) => message.id),
|
|
153
|
+
["a:32"],
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const previous_page = await session.history({
|
|
157
|
+
archive_id: current_page.previous_archive_id,
|
|
158
|
+
});
|
|
159
|
+
assert.equal(previous_page.archive_id, "compact-B");
|
|
160
|
+
assert.equal(previous_page.previous_archive_id, "compact-A");
|
|
161
|
+
assert.equal(previous_page.total, 2);
|
|
162
|
+
assert.deepEqual(
|
|
163
|
+
previous_page.items.map((message) => message.id),
|
|
164
|
+
["u:16", "a:17"],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const oldest_page = await session.history({
|
|
168
|
+
archive_id: previous_page.previous_archive_id,
|
|
169
|
+
});
|
|
170
|
+
assert.equal(oldest_page.archive_id, "compact-A");
|
|
171
|
+
assert.equal(oldest_page.previous_archive_id, undefined);
|
|
172
|
+
assert.deepEqual(
|
|
173
|
+
oldest_page.items.map((message) => message.id),
|
|
174
|
+
["u:1", "a:2"],
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const timeline_page = await session.history({
|
|
178
|
+
view: "timeline",
|
|
179
|
+
archive_id: "compact-B",
|
|
180
|
+
});
|
|
181
|
+
assert.deepEqual(
|
|
182
|
+
timeline_page.items.map((event) => event.text),
|
|
183
|
+
["user 16", "assistant 17"],
|
|
184
|
+
);
|
|
185
|
+
} finally {
|
|
186
|
+
await agent.dispose();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
@@ -191,6 +191,7 @@ export class HttpRemoteAgentTransport implements RemoteAgentTransport {
|
|
|
191
191
|
const query = new URLSearchParams();
|
|
192
192
|
if (input?.limit !== undefined) query.set("limit", String(input.limit));
|
|
193
193
|
if (input?.cursor) query.set("cursor", input.cursor);
|
|
194
|
+
if (input?.archive_id) query.set("archive_id", input.archive_id);
|
|
194
195
|
if (input?.order) query.set("order", input.order);
|
|
195
196
|
if (input?.view) query.set("view", input.view);
|
|
196
197
|
const payload = await read_http_json<{
|
|
@@ -39,6 +39,7 @@ type SessionCompactDeps = {
|
|
|
39
39
|
loadAll: () => Promise<SessionMessageV1[]>;
|
|
40
40
|
createSummaryMessage: (params: {
|
|
41
41
|
text: string;
|
|
42
|
+
archiveId?: string;
|
|
42
43
|
sourceRange?: SessionMetadataV1["sourceRange"];
|
|
43
44
|
}) => SessionMessageV1;
|
|
44
45
|
getArchiveDirPath: () => string;
|
|
@@ -141,13 +142,13 @@ export async function compactSessionMessagesIfNeeded(
|
|
|
141
142
|
|
|
142
143
|
const fromId = String(older[0]?.id || "");
|
|
143
144
|
const toId = String(older[older.length - 1]?.id || "");
|
|
145
|
+
const archiveId = `compact-${Date.now()}-${generateId()}`;
|
|
144
146
|
const summaryMsg = deps.createSummaryMessage({
|
|
145
147
|
text: summary,
|
|
148
|
+
archiveId,
|
|
146
149
|
sourceRange: fromId && toId ? { fromId, toId, count: older.length } : undefined,
|
|
147
150
|
});
|
|
148
151
|
|
|
149
|
-
const archiveId = `compact-${Date.now()}-${generateId()}`;
|
|
150
|
-
|
|
151
152
|
// phase 2:写入(短锁,且避免覆盖新追加)
|
|
152
153
|
// - 以“当前最新 session messages”为准重算 currentOlder / currentKept,避免覆盖并发新消息。
|
|
153
154
|
await deps.withWriteLock(async () => {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
isTextUIPart,
|
|
17
17
|
type FileUIPart,
|
|
18
18
|
} from "ai";
|
|
19
|
+
import type { SessionUserMessagePart } from "@/types/sdk/AgentSessionPrompt.js";
|
|
19
20
|
import type { SessionMessageV1 } from "@/executor/types/SessionMessages.js";
|
|
20
21
|
import { parseChatMessageMarkup } from "@/executor/messages/ChatMessageMarkup.js";
|
|
21
22
|
|
|
@@ -102,6 +103,66 @@ async function hydrateFileUrlPart(
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
async function hydrateFileUrlPartStrict(
|
|
107
|
+
part: FileUIPart,
|
|
108
|
+
project_root?: string,
|
|
109
|
+
): Promise<FileUIPart> {
|
|
110
|
+
const url = String(part.url || "").trim();
|
|
111
|
+
const file_path = resolveHydratableFilePath(project_root, url);
|
|
112
|
+
if (!file_path) return part;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const buffer = await fs.readFile(file_path);
|
|
116
|
+
const media_type =
|
|
117
|
+
String(part.mediaType || "").trim() ||
|
|
118
|
+
guessAttachmentMediaTypeFromPath(file_path) ||
|
|
119
|
+
"application/octet-stream";
|
|
120
|
+
return {
|
|
121
|
+
...part,
|
|
122
|
+
mediaType: media_type,
|
|
123
|
+
url: buildDataUrl(media_type, buffer),
|
|
124
|
+
};
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
+
throw new Error(`读取本地附件失败:${file_path}。${message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 在用户 prompt 入库前,将本地图片 file part 转成 data URL。
|
|
133
|
+
*
|
|
134
|
+
* 关键点(中文)
|
|
135
|
+
* - 只处理调用侧直接传入的图片 file part。
|
|
136
|
+
* - 已经是 data URL 或远程 URL 的附件保持原样。
|
|
137
|
+
* - 本地文件读取失败时直接报错,避免模型请求拿到不可访问的本地路径。
|
|
138
|
+
*/
|
|
139
|
+
export async function hydrateUserPromptFileParts(
|
|
140
|
+
parts: SessionUserMessagePart[],
|
|
141
|
+
project_root?: string,
|
|
142
|
+
): Promise<SessionUserMessagePart[]> {
|
|
143
|
+
if (!Array.isArray(parts) || parts.length === 0) return [];
|
|
144
|
+
|
|
145
|
+
const out: SessionUserMessagePart[] = [];
|
|
146
|
+
for (const part of parts) {
|
|
147
|
+
if (!isFileUIPart(part as FileUIPart)) {
|
|
148
|
+
out.push(part);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const file_part = part as FileUIPart;
|
|
153
|
+
const media_type = String(file_part.mediaType || "").trim();
|
|
154
|
+
if (!media_type.startsWith("image/")) {
|
|
155
|
+
out.push(part);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const next_part = await hydrateFileUrlPartStrict(file_part, project_root);
|
|
160
|
+
out.push(next_part as SessionUserMessagePart);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
105
166
|
/**
|
|
106
167
|
* 将历史中的资源 file part 临时转换为模型可消费的 data URL。
|
|
107
168
|
*
|
|
@@ -445,7 +445,7 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
|
|
|
445
445
|
sessionId: this.sessionId,
|
|
446
446
|
withWriteLock: (fn) => this.withWriteLock(fn),
|
|
447
447
|
loadAll: () => this.list(),
|
|
448
|
-
createSummaryMessage: ({ text, sourceRange }) => ({
|
|
448
|
+
createSummaryMessage: ({ text, archiveId, sourceRange }) => ({
|
|
449
449
|
id: `a:${this.sessionId}:${generateId()}`,
|
|
450
450
|
role: "assistant",
|
|
451
451
|
metadata: {
|
|
@@ -454,6 +454,7 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
|
|
|
454
454
|
sessionId: this.sessionId,
|
|
455
455
|
source: "compact",
|
|
456
456
|
kind: "summary",
|
|
457
|
+
...(archiveId ? { archiveId } : {}),
|
|
457
458
|
...(sourceRange ? { sourceRange } : {}),
|
|
458
459
|
},
|
|
459
460
|
parts: [{ type: "text", text: String(text ?? "") }],
|
|
@@ -64,6 +64,8 @@ export type SessionMetadataV1 = {
|
|
|
64
64
|
kind?: SessionMessageKind;
|
|
65
65
|
/** 当前消息来自入站、出站还是 compact。 */
|
|
66
66
|
source?: SessionMessageSource;
|
|
67
|
+
/** compact 摘要对应的 archive 文件 ID,用于用户历史按层读取更早消息。 */
|
|
68
|
+
archiveId?: string;
|
|
67
69
|
/** compact 摘要所覆盖的原始消息范围。 */
|
|
68
70
|
sourceRange?: SessionMessageSourceRangeV1;
|
|
69
71
|
/**
|
|
@@ -83,6 +83,13 @@ type SessionBrowseBaseInput = {
|
|
|
83
83
|
executing?: boolean;
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
+
type SessionArchiveFileV1 = {
|
|
87
|
+
v?: unknown;
|
|
88
|
+
sessionId?: unknown;
|
|
89
|
+
archivedAt?: unknown;
|
|
90
|
+
messages?: unknown;
|
|
91
|
+
};
|
|
92
|
+
|
|
86
93
|
function decodeMaybe(input: string): string {
|
|
87
94
|
try {
|
|
88
95
|
return decodeURIComponent(input);
|
|
@@ -122,6 +129,11 @@ function encodeCursor(offset: number): string | undefined {
|
|
|
122
129
|
return String(Math.floor(offset));
|
|
123
130
|
}
|
|
124
131
|
|
|
132
|
+
function normalizeArchiveId(input: unknown): string | undefined {
|
|
133
|
+
const value = String(input || "").trim();
|
|
134
|
+
return value || undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
125
137
|
function stringifyForDisplay(input: unknown, maxChars = 2400): string {
|
|
126
138
|
if (input === undefined) return "";
|
|
127
139
|
if (input === null) return "null";
|
|
@@ -359,6 +371,53 @@ export async function loadSessionMessagesFromPath(
|
|
|
359
371
|
return messages;
|
|
360
372
|
}
|
|
361
373
|
|
|
374
|
+
/**
|
|
375
|
+
* 读取 compact archive 文件中的消息。
|
|
376
|
+
*
|
|
377
|
+
* 关键点(中文)
|
|
378
|
+
* - archive 是 compact 时被覆盖的“上一层历史”,不是当前运行态事实源。
|
|
379
|
+
* - 这里不读取 inflight,因为 archive 层永远是已完成的历史快照。
|
|
380
|
+
*/
|
|
381
|
+
export async function loadSessionArchiveMessagesFromPath(
|
|
382
|
+
filePath: string,
|
|
383
|
+
): Promise<SessionMessageV1[]> {
|
|
384
|
+
try {
|
|
385
|
+
const raw = (await fs.readJson(filePath)) as SessionArchiveFileV1 | null;
|
|
386
|
+
const items = Array.isArray(raw?.messages) ? raw.messages : [];
|
|
387
|
+
return items.filter((item): item is SessionMessageV1 => {
|
|
388
|
+
if (!item || typeof item !== "object") return false;
|
|
389
|
+
const candidate = item as Partial<SessionMessageV1>;
|
|
390
|
+
return (
|
|
391
|
+
(candidate.role === "user" || candidate.role === "assistant") &&
|
|
392
|
+
Array.isArray(candidate.parts)
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
} catch {
|
|
396
|
+
return [];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function isCompactSummaryMessage(message: SessionMessageV1): boolean {
|
|
401
|
+
const metadata = (message.metadata || null) as SessionMetadataV1 | null;
|
|
402
|
+
return metadata?.source === "compact" || metadata?.kind === "summary";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function resolvePreviousArchiveId(messages: SessionMessageV1[]): string | undefined {
|
|
406
|
+
for (const message of messages) {
|
|
407
|
+
if (!isCompactSummaryMessage(message)) continue;
|
|
408
|
+
const metadata = (message.metadata || null) as SessionMetadataV1 | null;
|
|
409
|
+
const archiveId = normalizeArchiveId(metadata?.archiveId);
|
|
410
|
+
if (archiveId) return archiveId;
|
|
411
|
+
}
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function filterUserVisibleHistoryMessages(
|
|
416
|
+
messages: SessionMessageV1[],
|
|
417
|
+
): SessionMessageV1[] {
|
|
418
|
+
return messages.filter((message) => !isCompactSummaryMessage(message));
|
|
419
|
+
}
|
|
420
|
+
|
|
362
421
|
/**
|
|
363
422
|
* 基于 metadata + messages 构建 SDK session 详情。
|
|
364
423
|
*/
|
|
@@ -409,9 +468,12 @@ export function buildSessionHistoryPage(params: {
|
|
|
409
468
|
const order = params.input?.order || "asc";
|
|
410
469
|
const limit = normalizeLimit(params.input?.limit, 50, 500);
|
|
411
470
|
const cursor = normalizeCursor(params.input?.cursor);
|
|
471
|
+
const archive_id = normalizeArchiveId(params.input?.archive_id);
|
|
472
|
+
const previous_archive_id = resolvePreviousArchiveId(params.messages);
|
|
473
|
+
const visibleMessages = filterUserVisibleHistoryMessages(params.messages);
|
|
412
474
|
|
|
413
475
|
if (view === "timeline") {
|
|
414
|
-
const allEvents =
|
|
476
|
+
const allEvents = visibleMessages.flatMap((message) => toSessionTimelineEvents(message));
|
|
415
477
|
const orderedEvents = order === "desc" ? [...allEvents].reverse() : allEvents;
|
|
416
478
|
const pageItems = orderedEvents.slice(cursor, cursor + limit);
|
|
417
479
|
const nextOffset = cursor + pageItems.length;
|
|
@@ -421,14 +483,16 @@ export function buildSessionHistoryPage(params: {
|
|
|
421
483
|
items: pageItems,
|
|
422
484
|
total: orderedEvents.length,
|
|
423
485
|
...(nextOffset < orderedEvents.length
|
|
424
|
-
? {
|
|
486
|
+
? { next_cursor: encodeCursor(nextOffset) }
|
|
425
487
|
: {}),
|
|
426
|
-
|
|
488
|
+
has_more: nextOffset < orderedEvents.length,
|
|
489
|
+
...(archive_id ? { archive_id } : {}),
|
|
490
|
+
...(previous_archive_id ? { previous_archive_id } : {}),
|
|
427
491
|
};
|
|
428
492
|
}
|
|
429
493
|
|
|
430
494
|
const orderedMessages =
|
|
431
|
-
order === "desc" ? [...
|
|
495
|
+
order === "desc" ? [...visibleMessages].reverse() : [...visibleMessages];
|
|
432
496
|
const pageItems = orderedMessages.slice(cursor, cursor + limit);
|
|
433
497
|
const nextOffset = cursor + pageItems.length;
|
|
434
498
|
return {
|
|
@@ -437,9 +501,11 @@ export function buildSessionHistoryPage(params: {
|
|
|
437
501
|
items: pageItems,
|
|
438
502
|
total: orderedMessages.length,
|
|
439
503
|
...(nextOffset < orderedMessages.length
|
|
440
|
-
? {
|
|
504
|
+
? { next_cursor: encodeCursor(nextOffset) }
|
|
441
505
|
: {}),
|
|
442
|
-
|
|
506
|
+
has_more: nextOffset < orderedMessages.length,
|
|
507
|
+
...(archive_id ? { archive_id } : {}),
|
|
508
|
+
...(previous_archive_id ? { previous_archive_id } : {}),
|
|
443
509
|
};
|
|
444
510
|
}
|
|
445
511
|
|
package/src/session/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export {
|
|
|
16
16
|
getSdkAgentArchivedSessionMetaPath,
|
|
17
17
|
getSdkAgentArchivedSessionsDirPath,
|
|
18
18
|
getSdkAgentSessionArchiveDirPath,
|
|
19
|
+
getSdkAgentSessionArchiveFilePath,
|
|
19
20
|
getSdkAgentSessionDirPath,
|
|
20
21
|
getSdkAgentSessionInflightPath,
|
|
21
22
|
getSdkAgentSessionMessagesDirPath,
|
|
@@ -49,6 +50,7 @@ export {
|
|
|
49
50
|
buildSessionInfo,
|
|
50
51
|
listArchivedAgentSessionSummaryPage,
|
|
51
52
|
listAgentSessionSummaryPage,
|
|
53
|
+
loadSessionArchiveMessagesFromPath,
|
|
52
54
|
loadSessionMessagesFromPath,
|
|
53
55
|
resolveSessionMessagePreview,
|
|
54
56
|
toSessionTimelineEvents,
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "@/session/index.js";
|
|
21
21
|
import { ensureSessionTitle } from "@/session/SessionTitle.js";
|
|
22
22
|
import { persistSdkAssistantResult } from "@/session/storage/Persistence.js";
|
|
23
|
+
import { hydrateUserPromptFileParts } from "@executor/messages/SessionAttachmentMapper.js";
|
|
23
24
|
import type { Executor } from "@executor/Executor.js";
|
|
24
25
|
import type { JsonlSessionHistoryStore } from "@/executor/store/history/jsonl/JsonlSessionHistoryStore.js";
|
|
25
26
|
import type {
|
|
@@ -324,8 +325,11 @@ export class SessionStateService {
|
|
|
324
325
|
return message;
|
|
325
326
|
}
|
|
326
327
|
|
|
327
|
-
// query 是 parts
|
|
328
|
-
const parts =
|
|
328
|
+
// query 是 parts 数组,先把本地图片附件转换为模型可消费的 data URL。
|
|
329
|
+
const parts = await hydrateUserPromptFileParts(
|
|
330
|
+
Array.isArray(query) ? query : [],
|
|
331
|
+
this.project_root,
|
|
332
|
+
);
|
|
329
333
|
const message: SessionUserMessageV1 = {
|
|
330
334
|
id: `u:${this.session_id}:${generateId()}`,
|
|
331
335
|
role: "user",
|
|
@@ -11,7 +11,9 @@ import { nanoid } from "nanoid";
|
|
|
11
11
|
import {
|
|
12
12
|
buildSessionHistoryPage,
|
|
13
13
|
buildSessionInfo,
|
|
14
|
+
getSdkAgentSessionArchiveFilePath,
|
|
14
15
|
ensureSessionTitle,
|
|
16
|
+
loadSessionArchiveMessagesFromPath,
|
|
15
17
|
readSessionMetadata,
|
|
16
18
|
} from "@/session/index.js";
|
|
17
19
|
import { buildSessionSystemBlocks } from "@/session/SessionSystemBuilder.js";
|
|
@@ -179,7 +181,8 @@ export class SessionViewService<TSession extends Pick<AgentSession, "set">> {
|
|
|
179
181
|
async history(
|
|
180
182
|
input?: AgentSessionHistoryInput,
|
|
181
183
|
): Promise<AgentSessionHistoryPage> {
|
|
182
|
-
const
|
|
184
|
+
const archive_id = String(input?.archive_id || "").trim();
|
|
185
|
+
const [metadata, current_messages] = await Promise.all([
|
|
183
186
|
readSessionMetadata({
|
|
184
187
|
projectRoot: this.project_root,
|
|
185
188
|
agentId: this.agent_id,
|
|
@@ -187,13 +190,23 @@ export class SessionViewService<TSession extends Pick<AgentSession, "set">> {
|
|
|
187
190
|
}),
|
|
188
191
|
this.history_store.list(),
|
|
189
192
|
]);
|
|
193
|
+
const page_messages = archive_id
|
|
194
|
+
? await loadSessionArchiveMessagesFromPath(
|
|
195
|
+
getSdkAgentSessionArchiveFilePath(
|
|
196
|
+
this.project_root,
|
|
197
|
+
this.agent_id,
|
|
198
|
+
this.session_id,
|
|
199
|
+
archive_id,
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
: current_messages;
|
|
190
203
|
const session = await this.build_info({
|
|
191
204
|
metadata,
|
|
192
|
-
messages,
|
|
205
|
+
messages: current_messages,
|
|
193
206
|
});
|
|
194
207
|
return buildSessionHistoryPage({
|
|
195
208
|
session,
|
|
196
|
-
messages,
|
|
209
|
+
messages: page_messages,
|
|
197
210
|
input,
|
|
198
211
|
});
|
|
199
212
|
}
|
|
@@ -185,6 +185,25 @@ export function getSdkAgentSessionArchiveDirPath(
|
|
|
185
185
|
);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* 单个 session 的 compact archive JSON 文件路径。
|
|
190
|
+
*
|
|
191
|
+
* 关键点(中文)
|
|
192
|
+
* - `archiveId` 来自 compact summary metadata,不由调用方拼接扩展名。
|
|
193
|
+
* - 这里统一 encode,避免 archiveId 中的特殊字符影响文件路径。
|
|
194
|
+
*/
|
|
195
|
+
export function getSdkAgentSessionArchiveFilePath(
|
|
196
|
+
projectRoot: string,
|
|
197
|
+
agentId: string,
|
|
198
|
+
sessionId: string,
|
|
199
|
+
archiveId: string,
|
|
200
|
+
): string {
|
|
201
|
+
return path.join(
|
|
202
|
+
getSdkAgentSessionArchiveDirPath(projectRoot, agentId, sessionId),
|
|
203
|
+
`${encodeURIComponent(String(archiveId || "").trim())}.json`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
188
207
|
/**
|
|
189
208
|
* 单个 session 的 inflight assistant 路径。
|
|
190
209
|
*
|
|
@@ -272,6 +272,14 @@ export interface AgentSessionHistoryInput {
|
|
|
272
272
|
limit?: number;
|
|
273
273
|
/** 分页游标。 */
|
|
274
274
|
cursor?: string;
|
|
275
|
+
/**
|
|
276
|
+
* 要读取的 compact archive 层 ID。
|
|
277
|
+
*
|
|
278
|
+
* 说明(中文)
|
|
279
|
+
* - 省略时读取当前 `messages.jsonl`。
|
|
280
|
+
* - 传入时读取 `messages/archive/<archive_id>.json` 中保存的上一层历史。
|
|
281
|
+
*/
|
|
282
|
+
archive_id?: string;
|
|
275
283
|
/**
|
|
276
284
|
* 返回顺序。
|
|
277
285
|
*
|
|
@@ -315,9 +323,25 @@ export interface AgentSessionHistoryPage {
|
|
|
315
323
|
*/
|
|
316
324
|
total: number;
|
|
317
325
|
/** 下一页游标。 */
|
|
318
|
-
|
|
326
|
+
next_cursor?: string;
|
|
319
327
|
/** 是否仍有更多数据。 */
|
|
320
|
-
|
|
328
|
+
has_more: boolean;
|
|
329
|
+
/**
|
|
330
|
+
* 当前读取的 compact archive 层 ID。
|
|
331
|
+
*
|
|
332
|
+
* 说明(中文)
|
|
333
|
+
* - 省略表示当前页来自主历史 `messages.jsonl`。
|
|
334
|
+
* - 有值表示当前页来自指定 compact archive。
|
|
335
|
+
*/
|
|
336
|
+
archive_id?: string;
|
|
337
|
+
/**
|
|
338
|
+
* 当前层更早一次 compact 对应的 archive ID。
|
|
339
|
+
*
|
|
340
|
+
* 关键点(中文)
|
|
341
|
+
* - compact summary 不会出现在 `items` 中。
|
|
342
|
+
* - 若当前层包含 compact summary,则这里暴露其 archive ID,供调用方继续 `session.history({ archive_id })`。
|
|
343
|
+
*/
|
|
344
|
+
previous_archive_id?: string;
|
|
321
345
|
}
|
|
322
346
|
|
|
323
347
|
/**
|