新增 memory 和 skill 存储,实现 Agent 持续学习,并增加工具支持;增加 LLM progress detail 输出

This commit is contained in:
2026-05-11 16:12:20 +08:00
parent a27c45910c
commit 5fbe8ae40c
16 changed files with 1411 additions and 129 deletions
+78
View File
@@ -3,8 +3,12 @@ import { randomUUID } from "node:crypto";
import { logger } from "../logger.js";
import { type OpencodeRuntimeAdapter } from "../runtime/opencode.js";
import { type SessionBinding, type SessionContext, SessionRegistry } from "../session/registry.js";
import { ToolSessionContextStore } from "../session/toolContextStore.js";
import { toActorKey, toProjectKey } from "../utils/fileStore.js";
export type ChatRequestContext = SessionContext & {
actorKey: string;
projectKey: string;
traceId: string;
};
@@ -12,6 +16,7 @@ export class ChatSessionBridge {
// 这里额外保存 session -> 用户上下文,供工具桥在服务端代发真实后端请求时复用。
private readonly sessionContexts = new Map<string, ChatRequestContext>();
private readonly sessionTitles = new Map<string, string>();
private readonly toolContextStore = new ToolSessionContextStore();
constructor(
private readonly registry: SessionRegistry,
@@ -23,6 +28,7 @@ export class ChatSessionBridge {
accessToken?: string;
projectId?: string;
traceId?: string;
userId?: string;
}): Promise<{
binding: SessionBinding;
requestContext: ChatRequestContext;
@@ -32,8 +38,11 @@ export class ChatSessionBridge {
clientSessionId:
context.clientSessionId?.trim() || `agent-${randomUUID().slice(0, 12)}`,
accessToken: context.accessToken,
actorKey: toActorKey(context.userId),
projectId: context.projectId,
projectKey: toProjectKey(context.projectId),
traceId: context.traceId?.trim() || `trace-${randomUUID().slice(0, 12)}`,
userId: context.userId?.trim(),
};
this.cleanupExpired();
@@ -41,6 +50,14 @@ export class ChatSessionBridge {
const current = this.registry.get(requestContext);
if (current) {
this.sessionContexts.set(current.sessionId, requestContext);
await this.toolContextStore.write({
actorKey: requestContext.actorKey,
clientSessionId: requestContext.clientSessionId,
projectId: requestContext.projectId,
projectKey: requestContext.projectKey,
sessionId: current.sessionId,
traceId: requestContext.traceId,
});
try {
// 只有 opencode 侧 session 仍存在时,才复用本地映射。
await this.runtime.getSession(current.sessionId);
@@ -60,6 +77,14 @@ export class ChatSessionBridge {
const session = await this.runtime.createSession(requestContext.clientSessionId);
const binding = this.registry.upsert(requestContext, session.id);
this.sessionContexts.set(binding.sessionId, requestContext);
await this.toolContextStore.write({
actorKey: requestContext.actorKey,
clientSessionId: requestContext.clientSessionId,
projectId: requestContext.projectId,
projectKey: requestContext.projectKey,
sessionId: binding.sessionId,
traceId: requestContext.traceId,
});
return { binding, requestContext, created: true };
}
@@ -83,11 +108,20 @@ export class ChatSessionBridge {
this.sessionTitles.set(sessionId, normalized);
}
cloneSessionTitle(sourceSessionId: string, targetSessionId: string) {
const existingTitle = this.sessionTitles.get(sourceSessionId);
if (!existingTitle) {
return;
}
this.sessionTitles.set(targetSessionId, existingTitle);
}
async abort(context: {
clientSessionId?: string;
accessToken?: string;
projectId?: string;
traceId?: string;
userId?: string;
}): Promise<SessionBinding | null> {
const clientSessionId = context.clientSessionId?.trim();
if (!clientSessionId) {
@@ -97,8 +131,11 @@ export class ChatSessionBridge {
const requestContext: ChatRequestContext = {
clientSessionId,
accessToken: context.accessToken,
actorKey: toActorKey(context.userId),
projectId: context.projectId,
projectKey: toProjectKey(context.projectId),
traceId: context.traceId?.trim() || `trace-${randomUUID().slice(0, 12)}`,
userId: context.userId?.trim(),
};
this.cleanupExpired();
@@ -109,6 +146,14 @@ export class ChatSessionBridge {
}
this.sessionContexts.set(binding.sessionId, requestContext);
await this.toolContextStore.write({
actorKey: requestContext.actorKey,
clientSessionId: requestContext.clientSessionId,
projectId: requestContext.projectId,
projectKey: requestContext.projectKey,
sessionId: binding.sessionId,
traceId: requestContext.traceId,
});
await this.runtime.abortSession(binding.sessionId);
return binding;
}
@@ -119,6 +164,7 @@ export class ChatSessionBridge {
projectId?: string;
traceId?: string;
keepMessageCount: number;
userId?: string;
}): Promise<{
binding: SessionBinding;
requestContext: ChatRequestContext;
@@ -128,8 +174,11 @@ export class ChatSessionBridge {
const nextRequestContext: ChatRequestContext = {
clientSessionId: `agent-${randomUUID().slice(0, 12)}`,
accessToken: context.accessToken,
actorKey: toActorKey(context.userId),
projectId: context.projectId,
projectKey: toProjectKey(context.projectId),
traceId: context.traceId?.trim() || `trace-${randomUUID().slice(0, 12)}`,
userId: context.userId?.trim(),
};
this.cleanupExpired();
@@ -138,14 +187,25 @@ export class ChatSessionBridge {
const session = await this.runtime.createSession(nextRequestContext.clientSessionId);
const binding = this.registry.upsert(nextRequestContext, session.id);
this.sessionContexts.set(binding.sessionId, nextRequestContext);
await this.toolContextStore.write({
actorKey: nextRequestContext.actorKey,
clientSessionId: nextRequestContext.clientSessionId,
projectId: nextRequestContext.projectId,
projectKey: nextRequestContext.projectKey,
sessionId: binding.sessionId,
traceId: nextRequestContext.traceId,
});
return { binding, requestContext: nextRequestContext, created: true };
}
const currentContext: ChatRequestContext = {
clientSessionId: currentClientSessionId,
accessToken: context.accessToken,
actorKey: toActorKey(context.userId),
projectId: context.projectId,
projectKey: toProjectKey(context.projectId),
traceId: nextRequestContext.traceId,
userId: context.userId?.trim(),
};
const current = this.registry.get(currentContext);
@@ -153,6 +213,14 @@ export class ChatSessionBridge {
const session = await this.runtime.createSession(nextRequestContext.clientSessionId);
const binding = this.registry.upsert(nextRequestContext, session.id);
this.sessionContexts.set(binding.sessionId, nextRequestContext);
await this.toolContextStore.write({
actorKey: nextRequestContext.actorKey,
clientSessionId: nextRequestContext.clientSessionId,
projectId: nextRequestContext.projectId,
projectKey: nextRequestContext.projectKey,
sessionId: binding.sessionId,
traceId: nextRequestContext.traceId,
});
return { binding, requestContext: nextRequestContext, created: true };
}
@@ -173,6 +241,15 @@ export class ChatSessionBridge {
const session = await this.runtime.forkSession(current.sessionId, keepMessage.info.id);
const binding = this.registry.upsert(nextRequestContext, session.id);
this.sessionContexts.set(binding.sessionId, nextRequestContext);
await this.toolContextStore.write({
actorKey: nextRequestContext.actorKey,
clientSessionId: nextRequestContext.clientSessionId,
projectId: nextRequestContext.projectId,
projectKey: nextRequestContext.projectKey,
sessionId: binding.sessionId,
traceId: nextRequestContext.traceId,
});
this.cloneSessionTitle(current.sessionId, binding.sessionId);
return { binding, requestContext: nextRequestContext, created: true };
}
@@ -181,6 +258,7 @@ export class ChatSessionBridge {
for (const sessionId of expiredSessionIds) {
this.sessionContexts.delete(sessionId);
this.sessionTitles.delete(sessionId);
void this.toolContextStore.remove(sessionId);
// 这里用 abort 做轻量清理;即使失败,也不阻断本地过期回收。
void this.runtime.abortSession(sessionId).catch((error) => {
logger.debug({ sessionId, err: error }, "ignoring failed abort for expired session");