perf: 优化 Agent 流式会话处理
- 合并流式通知与持久化写入,限制浏览器会话缓存 - 增量投影轮次事件并稳定关联异步工具任务
This commit is contained in:
@@ -64,10 +64,15 @@ interface StartOptions {
|
||||
}
|
||||
|
||||
const STORAGE_VERSION = 2;
|
||||
const STREAM_NOTIFY_INTERVAL_MS = 50;
|
||||
const STREAM_PERSIST_INTERVAL_MS = 300;
|
||||
const MAX_RUNTIME_SESSIONS_PER_IDENTITY = 10;
|
||||
|
||||
const sessions = new Map<string, RuntimeSessionState>();
|
||||
const listeners = new Set<() => void>();
|
||||
const latestSessionIds = new Map<string, string>();
|
||||
const persistTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
let notifyTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
function clone<T>(value: T): T {
|
||||
const serialized = JSON.stringify(value);
|
||||
@@ -108,6 +113,24 @@ function notify() {
|
||||
}
|
||||
}
|
||||
|
||||
function notifyNow() {
|
||||
if (notifyTimer) {
|
||||
clearTimeout(notifyTimer);
|
||||
notifyTimer = undefined;
|
||||
}
|
||||
notify();
|
||||
}
|
||||
|
||||
function scheduleNotify() {
|
||||
if (notifyTimer || listeners.size === 0) {
|
||||
return;
|
||||
}
|
||||
notifyTimer = setTimeout(() => {
|
||||
notifyTimer = undefined;
|
||||
notify();
|
||||
}, STREAM_NOTIFY_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function persistSession(state: RuntimeSessionState) {
|
||||
const storage = safeSessionStorage();
|
||||
if (!storage) {
|
||||
@@ -118,7 +141,7 @@ function persistSession(state: RuntimeSessionState) {
|
||||
agentName: state.agentName,
|
||||
completed: state.completed,
|
||||
error: state.error,
|
||||
items: clone(state.items),
|
||||
items: state.items,
|
||||
prompt: state.prompt,
|
||||
roundId: state.roundId,
|
||||
sessionId: state.sessionId,
|
||||
@@ -136,6 +159,82 @@ function persistSession(state: RuntimeSessionState) {
|
||||
}
|
||||
}
|
||||
|
||||
function cancelPersistTimer(scopedSessionKey: string) {
|
||||
const timer = persistTimers.get(scopedSessionKey);
|
||||
if (!timer) {
|
||||
return;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
persistTimers.delete(scopedSessionKey);
|
||||
}
|
||||
|
||||
function removeSessionCache(
|
||||
scopedSessionKey: string,
|
||||
state: RuntimeSessionState,
|
||||
) {
|
||||
cancelPersistTimer(scopedSessionKey);
|
||||
sessions.delete(scopedSessionKey);
|
||||
try {
|
||||
safeSessionStorage()?.removeItem(
|
||||
storageKey(state.identity, state.sessionId),
|
||||
);
|
||||
} catch {
|
||||
// 本地缓存清理失败不影响服务端会话。
|
||||
}
|
||||
}
|
||||
|
||||
function pruneRuntimeSessions(identity: string, currentSessionId: string) {
|
||||
const scopedSessions = [...sessions.entries()]
|
||||
.filter(([, session]) => session.identity === identity)
|
||||
.sort((first, second) => second[1].updatedAt - first[1].updatedAt);
|
||||
if (scopedSessions.length <= MAX_RUNTIME_SESSIONS_PER_IDENTITY) {
|
||||
return;
|
||||
}
|
||||
const protectedKeys = new Set(
|
||||
scopedSessions
|
||||
.filter(
|
||||
([, session]) =>
|
||||
session.sending || session.sessionId === currentSessionId,
|
||||
)
|
||||
.map(([key]) => key),
|
||||
);
|
||||
let retainedCount = protectedKeys.size;
|
||||
for (const [key, session] of scopedSessions) {
|
||||
if (protectedKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
if (retainedCount < MAX_RUNTIME_SESSIONS_PER_IDENTITY) {
|
||||
retainedCount += 1;
|
||||
continue;
|
||||
}
|
||||
removeSessionCache(key, session);
|
||||
}
|
||||
}
|
||||
|
||||
function touchState(state: RuntimeSessionState) {
|
||||
state.updatedAt = Date.now();
|
||||
latestSessionIds.set(state.identity, state.sessionId);
|
||||
sessions.set(sessionKey(state.identity, state.sessionId), state);
|
||||
}
|
||||
|
||||
function scheduleStateUpdate(state: RuntimeSessionState) {
|
||||
touchState(state);
|
||||
const scopedSessionKey = sessionKey(state.identity, state.sessionId);
|
||||
if (!persistTimers.has(scopedSessionKey)) {
|
||||
persistTimers.set(
|
||||
scopedSessionKey,
|
||||
setTimeout(() => {
|
||||
persistTimers.delete(scopedSessionKey);
|
||||
const current = sessions.get(scopedSessionKey);
|
||||
if (current) {
|
||||
persistSession(current);
|
||||
}
|
||||
}, STREAM_PERSIST_INTERVAL_MS),
|
||||
);
|
||||
}
|
||||
scheduleNotify();
|
||||
}
|
||||
|
||||
function restoreSession(identity: string, sessionId: string) {
|
||||
if (!identity) {
|
||||
return undefined;
|
||||
@@ -179,11 +278,11 @@ function restoreSession(identity: string, sessionId: string) {
|
||||
}
|
||||
|
||||
function upsertState(state: RuntimeSessionState) {
|
||||
state.updatedAt = Date.now();
|
||||
latestSessionIds.set(state.identity, state.sessionId);
|
||||
sessions.set(sessionKey(state.identity, state.sessionId), state);
|
||||
touchState(state);
|
||||
cancelPersistTimer(sessionKey(state.identity, state.sessionId));
|
||||
persistSession(state);
|
||||
notify();
|
||||
pruneRuntimeSessions(state.identity, state.sessionId);
|
||||
notifyNow();
|
||||
}
|
||||
|
||||
function runningSession(identity = identityScope()) {
|
||||
@@ -272,11 +371,12 @@ function replaceAcceptedImages(
|
||||
onAgentChatCacheClear((identity) => {
|
||||
for (const [key, session] of sessions) {
|
||||
if (session.identity === identity) {
|
||||
cancelPersistTimer(key);
|
||||
sessions.delete(key);
|
||||
}
|
||||
}
|
||||
latestSessionIds.delete(identity);
|
||||
notify();
|
||||
notifyNow();
|
||||
});
|
||||
|
||||
export const agentChatRuntimeManager = {
|
||||
@@ -383,7 +483,7 @@ export const agentChatRuntimeManager = {
|
||||
void options.onInputAccepted?.();
|
||||
}
|
||||
applyAgentSseEnvelope(current.items, envelope, { roundId });
|
||||
upsertState(current);
|
||||
scheduleStateUpdate(current);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user