import { randomUUID } from "node:crypto"; import cors from "cors"; import express from "express"; import { SessionTranscriptStore } from "./sessions/transcriptStore.js"; import { ChatSessionBridge } from "./chat/sessionBridge.js"; import { config } from "./config.js"; import { SessionUiStateStore } from "./sessions/uiStateStore.js"; import { SessionMetadataStore } from "./sessions/metadataStore.js"; import { logger } from "./logger.js"; import { LearningOrchestrator } from "./learning/orchestrator.js"; import { MemoryStore } from "./memory/store.js"; import { ResultReferenceResolver } from "./results/resolver.js"; import { ResultReferenceStore } from "./results/store.js"; import { buildChatRouter } from "./routes/chat.js"; import { FrontendActionCoordinator, FrontendActionError } from "./frontendAction/coordinator.js"; import { opencodeRuntime } from "./runtime/opencode.js"; import { getRuntimeSessionContext } from "./runtime/sessionContext.js"; const app = express(); // 这里集中组装 Agent 服务的运行期依赖,路由层只通过接口调用,便于测试时替换实现。 const sessionBridge = new ChatSessionBridge(opencodeRuntime); const sessionMetadataStore = new SessionMetadataStore(); const sessionUiStateStore = new SessionUiStateStore(); const memoryStore = new MemoryStore(); const sessionTranscriptStore = new SessionTranscriptStore(); const learningOrchestrator = new LearningOrchestrator( opencodeRuntime, memoryStore, sessionTranscriptStore, ); const resultReferenceStore = new ResultReferenceStore(); const resultReferenceResolver = new ResultReferenceResolver(resultReferenceStore); const internalToken = config.AGENT_INTERNAL_TOKEN ?? randomUUID(); const frontendActionCoordinator = new FrontendActionCoordinator(); process.env.TJWATER_AGENT_INTERNAL_TOKEN = internalToken; app.use(cors()); app.use(express.json({ limit: "1mb" })); app.post("/internal/frontend-actions/request", async (req, res) => { if (req.header("x-agent-internal-token") !== internalToken) { res.status(403).json({ message: "forbidden" }); return; } const runtimeSessionId = typeof req.body?.session_id === "string" ? req.body.session_id.trim() : ""; const context = runtimeSessionId ? getRuntimeSessionContext(runtimeSessionId) : null; if (!context) { res.status(404).json({ message: "session context not found" }); return; } if (!context.capabilities?.includes("frontend-action@1")) { // Legacy clients still execute the existing UIEnvelope emitted from the tool call. res.json({ version: "frontend-action-result@1", status: "succeeded", output: { legacy: true }, detail: "legacy client executes the action from its UIEnvelope", }); return; } try { const result = await frontendActionCoordinator.request({ sessionId: context.clientSessionId, toolCallId: typeof req.body?.call_id === "string" ? req.body.call_id : "", name: typeof req.body?.name === "string" ? req.body.name : "", params: req.body?.params, fallbackText: typeof req.body?.fallback_text === "string" ? req.body.fallback_text : undefined, }); res.json(result); } catch (error) { const status = error instanceof FrontendActionError ? error.status : 500; res.status(status).json({ message: error instanceof Error ? error.message : String(error) }); } }); app.get("/health", async (_req, res) => { try { const runtime = await opencodeRuntime.health(); res.json({ ok: true, runtime, sessions: sessionBridge.count(), }); } catch (error) { const detail = error instanceof Error ? error.message : String(error); res.status(503).json({ ok: false, message: "opencode runtime unavailable", detail, sessions: sessionBridge.count(), }); } }); app.post("/internal/tools/session-search", async (req, res) => { if (req.header("x-agent-internal-token") !== internalToken) { res.status(403).json({ message: "forbidden" }); return; } const sessionId = typeof req.body?.session_id === "string" ? req.body.session_id.trim() : ""; const query = typeof req.body?.query === "string" ? req.body.query : ""; const context = sessionId ? getRuntimeSessionContext(sessionId) : null; if (!context) { res.status(404).json({ message: "session context not found", detail: sessionId, }); return; } if (!query.trim()) { res.status(400).json({ message: "query is required" }); return; } const hits = await sessionTranscriptStore.search( { actorKey: context.actorKey, projectKey: context.projectKey, }, query, typeof req.body?.max_results === "number" ? req.body.max_results : undefined, ); res.json({ hits, query, }); }); app.use( "/api/v1/agent/chat", buildChatRouter( sessionBridge, opencodeRuntime, sessionMetadataStore, sessionUiStateStore, memoryStore, sessionTranscriptStore, learningOrchestrator, resultReferenceResolver, frontendActionCoordinator, ), ); const bootstrap = async () => { await Promise.all([ sessionMetadataStore.initialize(), sessionUiStateStore.initialize(), learningOrchestrator.initialize(), memoryStore.initialize(), resultReferenceStore.initialize(), sessionTranscriptStore.initialize(), ]); resultReferenceStore.startCleanupLoop(); }; await bootstrap(); const server = app.listen(config.PORT, config.HOST, () => { logger.info( { host: config.HOST, port: config.PORT }, "TJWaterAgent listening", ); void warmupOpencodeRuntime(); }); const warmupOpencodeRuntime = async () => { const startedAt = Date.now(); try { await opencodeRuntime.ensureClient(); logger.info( { elapsedMs: Math.max(0, Date.now() - startedAt), mode: config.OPENCODE_MODE, }, "opencode runtime warmed up", ); } catch (error) { logger.error( { err: error, elapsedMs: Math.max(0, Date.now() - startedAt), mode: config.OPENCODE_MODE, }, "failed to warm up opencode runtime", ); } }; const shutdown = async () => { logger.info("shutting down TJWaterAgent"); server.close(); resultReferenceStore.stopCleanupLoop(); // 同步关闭 embedded opencode server,避免本服务退出后留下孤儿进程。 await opencodeRuntime.dispose(); }; process.on("SIGINT", () => { void shutdown(); }); process.on("SIGTERM", () => { void shutdown(); });