Skip to content
Merged

Dev #17

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

# 默认把挂载的用户目录作为工作区,会话和工具执行都落在里面
Expand Down
19 changes: 13 additions & 6 deletions packages/agent-core/src/harness/core/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? '';

Expand All @@ -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,
Expand All @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion packages/agent-core/src/harness/core/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface TaskState {
}

export interface CreateTaskStateOptions {
/** 复用已有会话时传入会话 ID,未传时生成新会话 ID */
id?: string;
mode?: AgentMode;
objective?: string;
constraints?: string[];
Expand All @@ -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 || '完成用户请求',
Expand Down
7 changes: 4 additions & 3 deletions packages/agent-core/src/harness/runtime/run-loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -150,4 +151,4 @@ export const runLoop = async (
}

return '已超出最大循环次数';
};
};
2 changes: 2 additions & 0 deletions packages/agent-core/src/harness/session/store-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/** 会话基本信息的最小形状 */
Expand Down
38 changes: 38 additions & 0 deletions packages/agent-core/src/web/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -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";
Expand All @@ -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,
};
}
Expand Down
31 changes: 31 additions & 0 deletions packages/agent-core/src/web/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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, {
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions packages/client/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Call Code History</title>
<script type="module" crossorigin src="./assets/index-Cp6GFlos.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DidvRlV2.css">
<script type="module" crossorigin src="./assets/index-DAwP6umD.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-CiGjvIya.css">
</head>
<body>
<div id="root"></div>
Expand Down
65 changes: 61 additions & 4 deletions packages/client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { connectLiveExport, type LiveExportConnection } from "./ws";
import type { AgentMode, ChatStatusMessage, Filter, Theme, WebExport } from "./types";
import { filterSessions } from "./utils";
import { ParticleField } from "./components/ParticleField";
import { ConfirmDialog } from "./components/ConfirmDialog";

const THEME_KEY = "call-code-theme";

Expand All @@ -14,6 +15,10 @@ const initialTheme = (): Theme => {
return stored === "light" ? "light" : "dark";
};

type PendingDelete =
| { kind: "session"; sessionId: string }
| { kind: "entry"; sessionId: string; entryId: string };

export default function App() {
const [data, setData] = useState<WebExport | null>(null);
const [loadState, setLoadState] = useState<"loading" | "ready" | "error">(
Expand All @@ -26,6 +31,9 @@ export default function App() {
const [activeId, setActiveId] = useState<string | null>(null);
const [query, setQuery] = useState("");
const [filter, setFilter] = useState<Filter>("all");
const [pendingDelete, setPendingDelete] = useState<PendingDelete | null>(
null,
);

const connectionRef = useRef<LiveExportConnection | null>(null);

Expand Down Expand Up @@ -94,7 +102,42 @@ export default function App() {
if (!connectionRef.current) {
return false;
}
return connectionRef.current.sendMessage(payload);
return connectionRef.current.sendMessage({
...payload,
sessionId: activeSession?.id,
});
};

const handleNewTopic = async () => {
const sessionId = await connectionRef.current?.createSession();
if (sessionId) {
setActiveId(sessionId);
setQuery("");
setFilter("all");
}
};

const requestDeleteEntry = (sessionId: string, entryId: string) => {
setPendingDelete({ kind: "entry", sessionId, entryId });
};

const requestDeleteSession = (sessionId: string) => {
setPendingDelete({ kind: "session", sessionId });
};

const confirmDelete = () => {
if (!pendingDelete) {
return;
}

if (pendingDelete.kind === "entry") {
connectionRef.current?.deleteMessages(pendingDelete.sessionId, [
pendingDelete.entryId,
]);
} else {
connectionRef.current?.deleteMessages(pendingDelete.sessionId);
}
setPendingDelete(null);
};

const sessions = useMemo(() => data?.sessions ?? [], [data]);
Expand All @@ -104,10 +147,10 @@ export default function App() {
);
const activeSession = useMemo(
() =>
filteredSessions.find((session) => session.id === activeId) ??
filteredSessions[0] ??
sessions.find((session) => session.id === activeId) ??
sessions[0] ??
null,
[filteredSessions, activeId],
[sessions, activeId],
);

return (
Expand All @@ -127,16 +170,30 @@ export default function App() {
query={query}
onSelect={setActiveId}
onQueryChange={setQuery}
onDeleteSession={requestDeleteSession}
onNewTopic={handleNewTopic}
/>
<MainPanel
session={activeSession}
filter={filter}
onFilterChange={setFilter}
chatStatus={chatStatus}
onSendMessage={handleSendMessage}
onDeleteEntry={requestDeleteEntry}
/>
</div>
</div>
<ConfirmDialog
open={pendingDelete !== null}
title={pendingDelete?.kind === "session" ? "删除会话" : "删除消息"}
description={
pendingDelete?.kind === "session"
? "删除整个会话?此操作无法撤销。"
: "删除这条消息及其后续回复?"
}
onConfirm={confirmDelete}
onCancel={() => setPendingDelete(null)}
/>
</div>
);
}
Loading
Loading