diff --git a/Dockerfile b/Dockerfile index 72e2e97..e94d57a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,12 +15,14 @@ WORKDIR /app COPY package.json bun.lock bunfig.toml tsconfig.json ./ COPY packages/agent-core/package.json packages/agent-core/ COPY packages/client/package.json packages/client/ +COPY packages/server/package.json packages/server/ COPY packages/session-sqlite/package.json packages/session-sqlite/ RUN bun install --frozen-lockfile COPY source source COPY packages/agent-core packages/agent-core +COPY packages/server packages/server COPY packages/session-sqlite packages/session-sqlite # 默认把挂载的用户目录作为工作区,会话和工具执行都落在里面 diff --git a/packages/agent-core/src/harness/core/llm.ts b/packages/agent-core/src/harness/core/llm.ts index d591095..42e3796 100644 --- a/packages/agent-core/src/harness/core/llm.ts +++ b/packages/agent-core/src/harness/core/llm.ts @@ -17,10 +17,17 @@ export interface StreamHandlers { onTrace?: (message: string) => void; } -const client = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, - baseURL: process.env.OPENAI_API_BASE_URL, -}); +let client: OpenAI | null = null; + +function getClient(): OpenAI { + if (!client) { + client = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, + baseURL: process.env.OPENAI_API_BASE_URL, + }); + } + return client; +} export const llmModel = process.env.OPENAI_MODEL ?? ''; @@ -32,7 +39,7 @@ export async function callLLM(messages: Message[]) { throw new Error(missingModelMessage); } - const res = await client.chat.completions.create({ + const res = await getClient().chat.completions.create({ model: llmModel, messages, temperature: 0, @@ -52,7 +59,7 @@ export async function streamLLM( throw new Error(missingModelMessage); } - const stream = await client.chat.completions.create({ + const stream = await getClient().chat.completions.create({ model: llmModel, messages, temperature: 0, diff --git a/packages/agent-core/src/harness/core/state.ts b/packages/agent-core/src/harness/core/state.ts index ef45996..61777b4 100644 --- a/packages/agent-core/src/harness/core/state.ts +++ b/packages/agent-core/src/harness/core/state.ts @@ -13,6 +13,8 @@ export interface TaskState { } export interface CreateTaskStateOptions { + /** 复用已有会话时传入会话 ID,未传时生成新会话 ID */ + id?: string; mode?: AgentMode; objective?: string; constraints?: string[]; @@ -36,7 +38,7 @@ export const createTaskState = ( const trimmedInput = input.trim(); return { - id: randomUUID(), + id: options.id?.trim() || randomUUID(), input, mode: options.mode ?? 'build', objective: options.objective?.trim() || trimmedInput || '完成用户请求', diff --git a/packages/agent-core/src/harness/runtime/run-loop.ts b/packages/agent-core/src/harness/runtime/run-loop.ts index 4721afd..cf5b700 100644 --- a/packages/agent-core/src/harness/runtime/run-loop.ts +++ b/packages/agent-core/src/harness/runtime/run-loop.ts @@ -127,10 +127,11 @@ export const runLoop = async ( if (!res) { return '无法获取 LLM 回复'; } + const parsed = parseAgentResponse(res); history.push({ role: 'assistant', content: res }); - session.appendAssistant(res); + // 会话里保存人类可读的 message,避免 WebUI 展示原始协议 JSON + session.appendAssistant(parsed ? parsed.message : extractFinalText(res)); - const parsed = parseAgentResponse(res); if (parsed && isToolCallAction(parsed)) { const execution = await runToolCall(task, parsed, session); history.push({ role: 'user', content: execution.content }); @@ -150,4 +151,4 @@ export const runLoop = async ( } return '已超出最大循环次数'; -}; \ No newline at end of file +}; diff --git a/packages/agent-core/src/harness/session/store-types.ts b/packages/agent-core/src/harness/session/store-types.ts index a3a419d..02fb4cc 100644 --- a/packages/agent-core/src/harness/session/store-types.ts +++ b/packages/agent-core/src/harness/session/store-types.ts @@ -48,6 +48,8 @@ export interface SessionStoreLike { options: { limit?: number }, ): EntryLike[]; listSessions(options: { limit?: number }): SessionLike[]; + deleteEntries(sessionId: string, entryIds: string[]): number; + deleteSession(sessionId: string): boolean; } /** 会话基本信息的最小形状 */ diff --git a/packages/agent-core/src/web/protocol.ts b/packages/agent-core/src/web/protocol.ts index d311d0e..1124373 100644 --- a/packages/agent-core/src/web/protocol.ts +++ b/packages/agent-core/src/web/protocol.ts @@ -4,6 +4,7 @@ import type { WebExport } from "./export.js"; export type ChatSendPayload = { type: "chat.send"; input: string; + sessionId?: string; mode?: AgentMode; objective?: string; constraints?: string[]; @@ -13,11 +14,14 @@ export type ChatSendPayload = { /** 客户端发给 WebSocket 服务的请求。 */ export type WebSocketClientMessage = | { type: "sessions.list" } + | { type: "sessions.create" } + | { type: "sessions.delete"; sessionId: string; entryIds?: string[] } | ChatSendPayload; /** 服务端发给客户端的响应。 */ export type WebSocketServerMessage = | { type: "sessions.snapshot"; data: WebExport } + | { type: "sessions.created"; sessionId: string } | { type: "chat.status"; status: "idle" | "running" | "success" | "error"; @@ -41,16 +45,50 @@ export const parseClientMessage = ( return { type: "sessions.list" }; } + if (message.type === "sessions.create") { + return { type: "sessions.create" }; + } + + if (message.type === "sessions.delete") { + const rawSessionId = (message as { sessionId?: unknown }).sessionId; + if (typeof rawSessionId !== "string" || !rawSessionId.trim()) { + return null; + } + const rawEntryIds = (message as { entryIds?: unknown }).entryIds; + if ( + rawEntryIds !== undefined && + (!Array.isArray(rawEntryIds) || + rawEntryIds.some( + (id) => typeof id !== "string" || !id.trim(), + )) + ) { + return null; + } + return { + type: "sessions.delete", + sessionId: rawSessionId.trim(), + entryIds: rawEntryIds?.map((id) => id.trim()), + }; + } + if (message.type === "chat.send") { const rawInput = (message as { input?: unknown }).input; if (typeof rawInput !== "string" || !rawInput.trim()) { return null; } + const rawSessionId = (message as { sessionId?: unknown }).sessionId; + if ( + rawSessionId !== undefined && + (typeof rawSessionId !== "string" || !rawSessionId.trim()) + ) { + return null; + } const rawMode = (message as { mode?: unknown }).mode; const mode: AgentMode = rawMode === "plan" ? "plan" : "build"; return { type: "chat.send", input: rawInput.trim(), + sessionId: rawSessionId?.trim(), mode, }; } diff --git a/packages/agent-core/src/web/server.ts b/packages/agent-core/src/web/server.ts index 356e972..d5de505 100644 --- a/packages/agent-core/src/web/server.ts +++ b/packages/agent-core/src/web/server.ts @@ -2,6 +2,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht import { readFile, stat } from "node:fs/promises"; import { extname, join, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; +import { randomUUID } from "node:crypto"; import { WebSocket, WebSocketServer } from "ws"; import { runLoop } from "../harness/runtime/run-loop.js"; import { createTaskState } from "../harness/core/state.js"; @@ -170,6 +171,35 @@ export const startWebServer = async ( return; } + if (message.type === "sessions.create") { + const session = store.createSession({ + id: randomUUID(), + cwd: process.cwd(), + }); + sendJson(socket, { + type: "sessions.created", + sessionId: session.id, + }); + broadcast(wss, { + type: "sessions.snapshot", + data: buildWebExport(store), + }); + return; + } + + if (message.type === "sessions.delete") { + if (message.entryIds && message.entryIds.length > 0) { + store.deleteEntries(message.sessionId, message.entryIds); + } else { + store.deleteSession(message.sessionId); + } + broadcast(wss, { + type: "sessions.snapshot", + data: buildWebExport(store), + }); + return; + } + if (message.type === "chat.send") { if (isRunning) { sendJson(socket, { @@ -189,6 +219,7 @@ export const startWebServer = async ( try { const task = createTaskState(message.input, { + id: message.sessionId, mode: message.mode ?? "build", objective: message.objective, constraints: message.constraints, diff --git a/packages/client/dist/index.html b/packages/client/dist/index.html index c3ad76e..888ab4f 100644 --- a/packages/client/dist/index.html +++ b/packages/client/dist/index.html @@ -4,8 +4,8 @@
+ {description} +
+