refactor daemon bootstrap and add smoke test
This commit is contained in:
@@ -4,7 +4,6 @@ import path from "node:path";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { AgentStatusSchema } from "../messages.js";
|
import { AgentStatusSchema } from "../messages.js";
|
||||||
import { resolvePaseoHome } from "../config.js";
|
|
||||||
import { toStoredAgentRecord } from "./agent-projections.js";
|
import { toStoredAgentRecord } from "./agent-projections.js";
|
||||||
import type { ManagedAgent } from "./agent-manager.js";
|
import type { ManagedAgent } from "./agent-manager.js";
|
||||||
import type { AgentSessionConfig } from "./agent-sdk-types.js";
|
import type { AgentSessionConfig } from "./agent-sdk-types.js";
|
||||||
@@ -68,8 +67,8 @@ export class AgentRegistry {
|
|||||||
private loaded = false;
|
private loaded = false;
|
||||||
private filePath: string;
|
private filePath: string;
|
||||||
|
|
||||||
constructor(filePath?: string) {
|
constructor(filePath: string) {
|
||||||
this.filePath = filePath ?? path.join(resolvePaseoHome(), "agents.json");
|
this.filePath = filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(): Promise<StoredAgentRecord[]> {
|
async load(): Promise<StoredAgentRecord[]> {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const AgentStatusEnum = z.enum([
|
|||||||
"closed",
|
"closed",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const AGENT_WAIT_TIMEOUT_MS = 60000; // 60 seconds
|
const AGENT_WAIT_TIMEOUT_MS = 50000; // 50 seconds (surface friendly message before tool timeout)
|
||||||
|
|
||||||
function expandPath(path: string): string {
|
function expandPath(path: string): string {
|
||||||
if (path.startsWith("~/") || path === "~") {
|
if (path.startsWith("~/") || path === "~") {
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import type {
|
|||||||
AgentPromptInput,
|
AgentPromptInput,
|
||||||
AgentRunOptions,
|
AgentRunOptions,
|
||||||
AgentRunResult,
|
AgentRunResult,
|
||||||
AgentControlMcpConfig,
|
|
||||||
AgentSession,
|
AgentSession,
|
||||||
AgentSessionConfig,
|
AgentSessionConfig,
|
||||||
AgentStreamEvent,
|
AgentStreamEvent,
|
||||||
@@ -40,7 +39,6 @@ import type {
|
|||||||
ListPersistedAgentsOptions,
|
ListPersistedAgentsOptions,
|
||||||
PersistedAgentDescriptor,
|
PersistedAgentDescriptor,
|
||||||
} from "../agent-sdk-types.js";
|
} from "../agent-sdk-types.js";
|
||||||
import { resolvePaseoPort } from "../../config.js";
|
|
||||||
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
|
import { getOrchestratorModeInstructions } from "../orchestrator-instructions.js";
|
||||||
|
|
||||||
const CLAUDE_CAPABILITIES: AgentCapabilityFlags = {
|
const CLAUDE_CAPABILITIES: AgentCapabilityFlags = {
|
||||||
@@ -93,13 +91,6 @@ type ClaudeAgentSessionOptions = {
|
|||||||
handle?: AgentPersistenceHandle;
|
handle?: AgentPersistenceHandle;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_AGENT_CONTROL_MCP: AgentControlMcpConfig = {
|
|
||||||
url: `http://127.0.0.1:${resolvePaseoPort()}/mcp/agents`,
|
|
||||||
headers: {
|
|
||||||
Authorization: "Basic bW86Ym8=",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function appendCallerAgentId(url: string, agentId: string): string {
|
function appendCallerAgentId(url: string, agentId: string): string {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
@@ -563,8 +554,10 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Always include the agent-control MCP server so agents can launch other agents
|
// Always include the agent-control MCP server so agents can launch other agents
|
||||||
const agentControlConfig =
|
if (!this.config.agentControlMcp) {
|
||||||
this.config.agentControlMcp ?? DEFAULT_AGENT_CONTROL_MCP;
|
throw new Error("agentControlMcp is required for ClaudeAgentSession");
|
||||||
|
}
|
||||||
|
const agentControlConfig = this.config.agentControlMcp;
|
||||||
const agentControlUrl = this.managedAgentId
|
const agentControlUrl = this.managedAgentId
|
||||||
? appendCallerAgentId(agentControlConfig.url, this.managedAgentId)
|
? appendCallerAgentId(agentControlConfig.url, this.managedAgentId)
|
||||||
: agentControlConfig.url;
|
: agentControlConfig.url;
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import net from "node:net";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { mkdtemp, rm } from "node:fs/promises";
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import { createPaseoDaemon, type PaseoDaemonConfig } from "./bootstrap.js";
|
||||||
|
|
||||||
|
async function getAvailablePort(): Promise<number> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const server = net.createServer();
|
||||||
|
server.once("error", reject);
|
||||||
|
server.listen(0, () => {
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === "string") {
|
||||||
|
server.close(() => reject(new Error("Failed to acquire port")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { port } = address;
|
||||||
|
server.close(() => resolve(port));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("paseo daemon bootstrap", () => {
|
||||||
|
test("starts and serves health endpoint", async () => {
|
||||||
|
const paseoHome = await mkdtemp(path.join(os.tmpdir(), "paseo-home-"));
|
||||||
|
const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-"));
|
||||||
|
const port = await getAvailablePort();
|
||||||
|
const basicUsers = { test: "pass" };
|
||||||
|
const [agentMcpUser, agentMcpPassword] =
|
||||||
|
Object.entries(basicUsers)[0] ?? [];
|
||||||
|
const agentMcpAuthHeader =
|
||||||
|
agentMcpUser && agentMcpPassword
|
||||||
|
? `Basic ${Buffer.from(`${agentMcpUser}:${agentMcpPassword}`).toString("base64")}`
|
||||||
|
: undefined;
|
||||||
|
const agentMcpBearerToken =
|
||||||
|
agentMcpUser && agentMcpPassword
|
||||||
|
? Buffer.from(`${agentMcpUser}:${agentMcpPassword}`).toString("base64")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const daemonConfig: PaseoDaemonConfig = {
|
||||||
|
port,
|
||||||
|
paseoHome,
|
||||||
|
agentMcpRoute: "/mcp/agents",
|
||||||
|
agentMcpAllowedHosts: [`127.0.0.1:${port}`, `localhost:${port}`],
|
||||||
|
auth: {
|
||||||
|
basicUsers,
|
||||||
|
agentMcpAuthHeader,
|
||||||
|
agentMcpBearerToken,
|
||||||
|
realm: "Voice Assistant",
|
||||||
|
},
|
||||||
|
staticDir,
|
||||||
|
mcpDebug: false,
|
||||||
|
agentClients: {},
|
||||||
|
agentRegistryPath: path.join(paseoHome, "agents.json"),
|
||||||
|
agentControlMcp: {
|
||||||
|
url: `http://127.0.0.1:${port}/mcp/agents`,
|
||||||
|
...(agentMcpAuthHeader
|
||||||
|
? { headers: { Authorization: agentMcpAuthHeader } }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const daemon = await createPaseoDaemon(daemonConfig);
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
daemon.httpServer.listen(port, () => resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`http://127.0.0.1:${port}/api/health`, {
|
||||||
|
headers: agentMcpAuthHeader
|
||||||
|
? { Authorization: agentMcpAuthHeader }
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
expect(response.ok).toBe(true);
|
||||||
|
const payload = await response.json();
|
||||||
|
expect(payload.status).toBe("ok");
|
||||||
|
expect(typeof payload.timestamp).toBe("string");
|
||||||
|
} finally {
|
||||||
|
await daemon.close().catch(() => undefined);
|
||||||
|
await rm(paseoHome, { recursive: true, force: true });
|
||||||
|
await rm(staticDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
import express, { type Express } from "express";
|
||||||
|
import basicAuth from "express-basic-auth";
|
||||||
|
import { createServer as createHTTPServer, type Server as HTTPServer } from "http";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||||
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
|
import { VoiceAssistantWebSocketServer } from "./websocket-server.js";
|
||||||
|
import { initializeSTT, type STTConfig } from "./agent/stt-openai.js";
|
||||||
|
import { initializeTTS, type TTSConfig } from "./agent/tts-openai.js";
|
||||||
|
import { listConversations, deleteConversation } from "./persistence.js";
|
||||||
|
import { AgentManager } from "./agent/agent-manager.js";
|
||||||
|
import { AgentRegistry } from "./agent/agent-registry.js";
|
||||||
|
import { ClaudeAgentClient } from "./agent/providers/claude-agent.js";
|
||||||
|
import { CodexAgentClient } from "./agent/providers/codex-agent.js";
|
||||||
|
import { initializeTitleGenerator } from "../services/agent-title-generator.js";
|
||||||
|
import { attachAgentRegistryPersistence } from "./persistence-hooks.js";
|
||||||
|
import { createAgentMcpServer } from "./agent/mcp-server.js";
|
||||||
|
import type {
|
||||||
|
AgentClient,
|
||||||
|
AgentControlMcpConfig,
|
||||||
|
AgentProvider,
|
||||||
|
} from "./agent/agent-sdk-types.js";
|
||||||
|
|
||||||
|
type AgentMcpTransportMap = Map<string, StreamableHTTPServerTransport>;
|
||||||
|
|
||||||
|
export type PaseoAuthConfig = {
|
||||||
|
basicUsers: Record<string, string>;
|
||||||
|
realm?: string;
|
||||||
|
agentMcpBearerToken?: string;
|
||||||
|
agentMcpAuthHeader?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PaseoOpenAIConfig = {
|
||||||
|
apiKey?: string;
|
||||||
|
stt?: Partial<STTConfig> & { apiKey?: string };
|
||||||
|
tts?: Partial<TTSConfig> & { apiKey?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PaseoDaemonConfig = {
|
||||||
|
port: number;
|
||||||
|
paseoHome: string;
|
||||||
|
agentMcpRoute: string;
|
||||||
|
agentMcpAllowedHosts: string[];
|
||||||
|
auth: PaseoAuthConfig;
|
||||||
|
staticDir: string;
|
||||||
|
mcpDebug: boolean;
|
||||||
|
agentClients: Partial<Record<AgentProvider, AgentClient>>;
|
||||||
|
agentRegistryPath: string;
|
||||||
|
agentControlMcp: AgentControlMcpConfig;
|
||||||
|
openai?: PaseoOpenAIConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PaseoDaemonHandles = {
|
||||||
|
httpServer: HTTPServer;
|
||||||
|
app: Express;
|
||||||
|
wsServer: VoiceAssistantWebSocketServer;
|
||||||
|
agentManager: AgentManager;
|
||||||
|
agentRegistry: AgentRegistry;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function createPaseoDaemon(
|
||||||
|
config: PaseoDaemonConfig
|
||||||
|
): Promise<PaseoDaemonHandles> {
|
||||||
|
const agentMcpRoute = config.agentMcpRoute;
|
||||||
|
const basicAuthUsers = config.auth.basicUsers;
|
||||||
|
const staticDir = config.staticDir;
|
||||||
|
const authRealm = config.auth.realm ?? "Voice Assistant";
|
||||||
|
|
||||||
|
const agentMcpBearerToken = config.auth.agentMcpBearerToken;
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Serve static files from public directory (no auth required for APK downloads)
|
||||||
|
app.use("/public", express.static(staticDir));
|
||||||
|
|
||||||
|
// Basic authentication (skip for /public routes)
|
||||||
|
const basicAuthMiddleware = basicAuth({
|
||||||
|
users: basicAuthUsers,
|
||||||
|
challenge: true,
|
||||||
|
realm: authRealm,
|
||||||
|
});
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (agentMcpBearerToken && req.path.startsWith(agentMcpRoute)) {
|
||||||
|
const authHeader = req.header("authorization") ?? "";
|
||||||
|
if (authHeader.startsWith("Bearer ")) {
|
||||||
|
const token = authHeader.slice("Bearer ".length).trim();
|
||||||
|
if (token === agentMcpBearerToken) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return basicAuthMiddleware(req, res, next);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Health check endpoint
|
||||||
|
app.get("/api/health", (_req, res) => {
|
||||||
|
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Conversation management endpoints
|
||||||
|
app.get("/api/conversations", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const conversations = await listConversations();
|
||||||
|
res.json(conversations);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[API] Failed to list conversations:", error);
|
||||||
|
res.status(500).json({ error: "Failed to list conversations" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/api/conversations/:id", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
await deleteConversation(id);
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[API] Failed to delete conversation:", error);
|
||||||
|
res.status(500).json({ error: "Failed to delete conversation" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const httpServer = createHTTPServer(app);
|
||||||
|
|
||||||
|
const agentRegistry = new AgentRegistry(config.agentRegistryPath);
|
||||||
|
const agentManager = new AgentManager({
|
||||||
|
clients: {
|
||||||
|
claude: new ClaudeAgentClient(),
|
||||||
|
codex: new CodexAgentClient(),
|
||||||
|
...config.agentClients,
|
||||||
|
},
|
||||||
|
registry: agentRegistry,
|
||||||
|
agentControlMcp: config.agentControlMcp,
|
||||||
|
});
|
||||||
|
|
||||||
|
attachAgentRegistryPersistence(agentManager, agentRegistry);
|
||||||
|
const persistedRecords = await agentRegistry.list();
|
||||||
|
console.log(
|
||||||
|
`✓ Agent registry loaded (${persistedRecords.length} record${persistedRecords.length === 1 ? "" : "s"}); agents will initialize on demand`
|
||||||
|
);
|
||||||
|
|
||||||
|
const agentMcpTransports: AgentMcpTransportMap = new Map();
|
||||||
|
const allowedHosts = config.agentMcpAllowedHosts;
|
||||||
|
|
||||||
|
const createAgentMcpTransport = async (callerAgentId?: string) => {
|
||||||
|
const agentMcpServer = await createAgentMcpServer({
|
||||||
|
agentManager,
|
||||||
|
agentRegistry,
|
||||||
|
callerAgentId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const transport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: () => randomUUID(),
|
||||||
|
onsessioninitialized: (sessionId) => {
|
||||||
|
agentMcpTransports.set(sessionId, transport);
|
||||||
|
console.log(`[Agent MCP] Session initialized: ${sessionId}`);
|
||||||
|
},
|
||||||
|
onsessionclosed: (sessionId) => {
|
||||||
|
agentMcpTransports.delete(sessionId);
|
||||||
|
console.log(`[Agent MCP] Session closed: ${sessionId}`);
|
||||||
|
},
|
||||||
|
enableDnsRebindingProtection: true,
|
||||||
|
allowedHosts,
|
||||||
|
});
|
||||||
|
transport.onclose = () => {
|
||||||
|
if (transport.sessionId) {
|
||||||
|
agentMcpTransports.delete(transport.sessionId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
transport.onerror = (error) => {
|
||||||
|
console.error("[Agent MCP] Transport error:", error);
|
||||||
|
};
|
||||||
|
|
||||||
|
await agentMcpServer.connect(transport);
|
||||||
|
|
||||||
|
return transport;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAgentMcpRequest: express.RequestHandler = async (req, res) => {
|
||||||
|
if (config.mcpDebug) {
|
||||||
|
console.log("[Agent MCP] request", {
|
||||||
|
method: req.method,
|
||||||
|
url: req.originalUrl,
|
||||||
|
sessionId: req.header("mcp-session-id"),
|
||||||
|
authorization: req.header("authorization"),
|
||||||
|
body: req.body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const sessionId = req.header("mcp-session-id");
|
||||||
|
let transport = sessionId ? agentMcpTransports.get(sessionId) : undefined;
|
||||||
|
|
||||||
|
if (!transport) {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
res.status(400).json({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
error: {
|
||||||
|
code: -32000,
|
||||||
|
message: "Missing or invalid MCP session",
|
||||||
|
},
|
||||||
|
id: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isInitializeRequest(req.body)) {
|
||||||
|
res.status(400).json({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
error: {
|
||||||
|
code: -32000,
|
||||||
|
message: "Initialization request expected",
|
||||||
|
},
|
||||||
|
id: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const callerAgentIdRaw = req.query.callerAgentId;
|
||||||
|
const callerAgentId =
|
||||||
|
typeof callerAgentIdRaw === "string"
|
||||||
|
? callerAgentIdRaw
|
||||||
|
: Array.isArray(callerAgentIdRaw) && typeof callerAgentIdRaw[0] === "string"
|
||||||
|
? callerAgentIdRaw[0]
|
||||||
|
: undefined;
|
||||||
|
transport = await createAgentMcpTransport(callerAgentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await transport.handleRequest(req as any, res as any, req.body);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Agent MCP] Failed to handle request:", error);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({
|
||||||
|
jsonrpc: "2.0",
|
||||||
|
error: {
|
||||||
|
code: -32603,
|
||||||
|
message: "Internal MCP server error",
|
||||||
|
},
|
||||||
|
id: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
app.post(agentMcpRoute, handleAgentMcpRequest);
|
||||||
|
app.get(agentMcpRoute, handleAgentMcpRequest);
|
||||||
|
app.delete(agentMcpRoute, handleAgentMcpRequest);
|
||||||
|
console.log(`✓ Agent MCP server mounted at ${agentMcpRoute}`);
|
||||||
|
|
||||||
|
const wsServer = new VoiceAssistantWebSocketServer(
|
||||||
|
httpServer,
|
||||||
|
agentManager,
|
||||||
|
agentRegistry,
|
||||||
|
{
|
||||||
|
agentMcpUrl: config.agentControlMcp.url,
|
||||||
|
agentMcpHeaders: config.agentControlMcp.headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const openaiApiKey = config.openai?.apiKey;
|
||||||
|
if (openaiApiKey) {
|
||||||
|
console.log("✓ OpenAI client initialized");
|
||||||
|
|
||||||
|
const sttApiKey = config.openai?.stt?.apiKey ?? openaiApiKey;
|
||||||
|
if (sttApiKey) {
|
||||||
|
const { apiKey: _sttApiKey, ...sttConfig } = config.openai?.stt ?? {};
|
||||||
|
initializeSTT({
|
||||||
|
apiKey: sttApiKey,
|
||||||
|
...sttConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ttsApiKey = config.openai?.tts?.apiKey ?? openaiApiKey;
|
||||||
|
if (ttsApiKey) {
|
||||||
|
const { apiKey: _ttsApiKey, ...ttsConfig } = config.openai?.tts ?? {};
|
||||||
|
initializeTTS({
|
||||||
|
apiKey: ttsApiKey,
|
||||||
|
voice: "alloy",
|
||||||
|
model: "tts-1",
|
||||||
|
responseFormat: "pcm",
|
||||||
|
...ttsConfig,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeTitleGenerator(openaiApiKey);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"⚠ OPENAI_API_KEY not set - LLM, STT, and TTS features will not work"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = async () => {
|
||||||
|
await closeAllAgents(agentManager);
|
||||||
|
await wsServer.close();
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
httpServer.close(() => resolve());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
httpServer,
|
||||||
|
app,
|
||||||
|
wsServer,
|
||||||
|
agentManager,
|
||||||
|
agentRegistry,
|
||||||
|
close,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeAllAgents(agentManager: AgentManager): Promise<void> {
|
||||||
|
const agents = agentManager.listAgents();
|
||||||
|
for (const agent of agents) {
|
||||||
|
try {
|
||||||
|
await agentManager.closeAgent(agent.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Agents] Failed to close agent ${agent.id}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,9 @@ import os from "node:os";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { mkdirSync } from "node:fs";
|
import { mkdirSync } from "node:fs";
|
||||||
|
|
||||||
let cachedHomeDir: string | null = null;
|
import type { PaseoDaemonConfig } from "./bootstrap.js";
|
||||||
let cachedPort: number | null = null;
|
import type { STTConfig } from "./agent/stt-openai.js";
|
||||||
|
import type { TTSConfig } from "./agent/tts-openai.js";
|
||||||
|
|
||||||
function expandHomeDir(input: string): string {
|
function expandHomeDir(input: string): string {
|
||||||
if (input.startsWith("~/")) {
|
if (input.startsWith("~/")) {
|
||||||
@@ -15,23 +16,102 @@ function expandHomeDir(input: string): string {
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolvePaseoHome(): string {
|
export function readPaseoHomeFromEnv(
|
||||||
if (cachedHomeDir) {
|
env: NodeJS.ProcessEnv = process.env
|
||||||
return cachedHomeDir;
|
): string {
|
||||||
}
|
const raw = env.PASEO_HOME ?? env.PASEO_HOME_DIR ?? "~/.paseo";
|
||||||
const raw = process.env.PASEO_HOME ?? process.env.PASEO_HOME_DIR ?? "~/.paseo";
|
|
||||||
const expanded = path.resolve(expandHomeDir(raw));
|
const expanded = path.resolve(expandHomeDir(raw));
|
||||||
mkdirSync(expanded, { recursive: true });
|
mkdirSync(expanded, { recursive: true });
|
||||||
cachedHomeDir = expanded;
|
return expanded;
|
||||||
return cachedHomeDir;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolvePaseoPort(): number {
|
export function readPaseoPortFromEnv(
|
||||||
if (cachedPort !== null) {
|
env: NodeJS.ProcessEnv = process.env
|
||||||
return cachedPort;
|
): number {
|
||||||
}
|
const raw = env.PASEO_PORT ?? env.PORT ?? "6767";
|
||||||
const raw = process.env.PASEO_PORT ?? process.env.PORT ?? "6767";
|
|
||||||
const parsed = Number.parseInt(raw, 10);
|
const parsed = Number.parseInt(raw, 10);
|
||||||
cachedPort = Number.isFinite(parsed) ? parsed : 6767;
|
return Number.isFinite(parsed) ? parsed : 6767;
|
||||||
return cachedPort;
|
}
|
||||||
|
|
||||||
|
const DEFAULT_BASIC_AUTH_USERS = { mo: "bo" } as const;
|
||||||
|
const DEFAULT_AGENT_MCP_ROUTE = "/mcp/agents";
|
||||||
|
|
||||||
|
function readOpenAIConfigFromEnv(env: NodeJS.ProcessEnv = process.env) {
|
||||||
|
const apiKey = env.OPENAI_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sttConfidenceThreshold = env.STT_CONFIDENCE_THRESHOLD
|
||||||
|
? parseFloat(env.STT_CONFIDENCE_THRESHOLD)
|
||||||
|
: undefined;
|
||||||
|
const sttModel = env.STT_MODEL as STTConfig["model"];
|
||||||
|
const ttsVoice = (env.TTS_VOICE || "alloy") as
|
||||||
|
| "alloy"
|
||||||
|
| "echo"
|
||||||
|
| "fable"
|
||||||
|
| "onyx"
|
||||||
|
| "nova"
|
||||||
|
| "shimmer";
|
||||||
|
const ttsModel = (env.TTS_MODEL || "tts-1") as "tts-1" | "tts-1-hd";
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiKey,
|
||||||
|
stt: {
|
||||||
|
apiKey,
|
||||||
|
confidenceThreshold: sttConfidenceThreshold,
|
||||||
|
...(sttModel ? { model: sttModel } : {}),
|
||||||
|
},
|
||||||
|
tts: {
|
||||||
|
apiKey,
|
||||||
|
voice: ttsVoice,
|
||||||
|
model: ttsModel,
|
||||||
|
responseFormat: "pcm" as TTSConfig["responseFormat"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPaseoDaemonConfigFromEnv(
|
||||||
|
env: NodeJS.ProcessEnv = process.env
|
||||||
|
): PaseoDaemonConfig {
|
||||||
|
const paseoHome = readPaseoHomeFromEnv(env);
|
||||||
|
const port = readPaseoPortFromEnv(env);
|
||||||
|
|
||||||
|
const basicUsers = DEFAULT_BASIC_AUTH_USERS;
|
||||||
|
const [agentMcpUser, agentMcpPassword] =
|
||||||
|
Object.entries(basicUsers)[0] ?? [];
|
||||||
|
const agentMcpAuthHeader =
|
||||||
|
agentMcpUser && agentMcpPassword
|
||||||
|
? `Basic ${Buffer.from(`${agentMcpUser}:${agentMcpPassword}`).toString("base64")}`
|
||||||
|
: undefined;
|
||||||
|
const agentMcpBearerToken =
|
||||||
|
agentMcpUser && agentMcpPassword
|
||||||
|
? Buffer.from(`${agentMcpUser}:${agentMcpPassword}`).toString("base64")
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const openai = readOpenAIConfigFromEnv(env);
|
||||||
|
|
||||||
|
return {
|
||||||
|
port,
|
||||||
|
paseoHome,
|
||||||
|
agentMcpRoute: DEFAULT_AGENT_MCP_ROUTE,
|
||||||
|
agentMcpAllowedHosts: [`127.0.0.1:${port}`, `localhost:${port}`],
|
||||||
|
auth: {
|
||||||
|
basicUsers,
|
||||||
|
agentMcpAuthHeader,
|
||||||
|
agentMcpBearerToken,
|
||||||
|
realm: "Voice Assistant",
|
||||||
|
},
|
||||||
|
mcpDebug: env.MCP_DEBUG === "1",
|
||||||
|
agentControlMcp: {
|
||||||
|
url: `http://127.0.0.1:${port}${DEFAULT_AGENT_MCP_ROUTE}`,
|
||||||
|
...(agentMcpAuthHeader
|
||||||
|
? { headers: { Authorization: agentMcpAuthHeader } }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
agentRegistryPath: path.join(paseoHome, "agents.json"),
|
||||||
|
staticDir: "public",
|
||||||
|
agentClients: {},
|
||||||
|
openai,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,355 +1,41 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import express from "express";
|
import { createPaseoDaemon } from "./bootstrap.js";
|
||||||
import basicAuth from "express-basic-auth";
|
import { buildPaseoDaemonConfigFromEnv } from "./config.js";
|
||||||
import { createServer as createHTTPServer } from "http";
|
|
||||||
import { randomUUID } from "node:crypto";
|
|
||||||
import { VoiceAssistantWebSocketServer } from "./websocket-server.js";
|
|
||||||
import { initializeSTT, type STTConfig } from "./agent/stt-openai.js";
|
|
||||||
import { initializeTTS } from "./agent/tts-openai.js";
|
|
||||||
import { listConversations, deleteConversation } from "./persistence.js";
|
|
||||||
import { AgentManager } from "./agent/agent-manager.js";
|
|
||||||
import { AgentRegistry } from "./agent/agent-registry.js";
|
|
||||||
import { ClaudeAgentClient } from "./agent/providers/claude-agent.js";
|
|
||||||
import { CodexAgentClient } from "./agent/providers/codex-agent.js";
|
|
||||||
import { resolvePaseoPort } from "./config.js";
|
|
||||||
import { initializeTitleGenerator } from "../services/agent-title-generator.js";
|
|
||||||
import { attachAgentRegistryPersistence } from "./persistence-hooks.js";
|
|
||||||
import { createAgentMcpServer } from "./agent/mcp-server.js";
|
|
||||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
||||||
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
||||||
|
|
||||||
type AgentMcpTransportMap = Map<
|
|
||||||
string,
|
|
||||||
StreamableHTTPServerTransport
|
|
||||||
>;
|
|
||||||
|
|
||||||
const BASIC_AUTH_USERS = { mo: "bo" } as const;
|
|
||||||
|
|
||||||
function createServer() {
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
const [agentMcpUser, agentMcpPassword] =
|
|
||||||
Object.entries(BASIC_AUTH_USERS)[0] ?? [];
|
|
||||||
const agentMcpBearerToken =
|
|
||||||
agentMcpUser && agentMcpPassword
|
|
||||||
? Buffer.from(`${agentMcpUser}:${agentMcpPassword}`).toString("base64")
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// Serve static files from public directory (no auth required for APK downloads)
|
|
||||||
app.use("/public", express.static("public"));
|
|
||||||
|
|
||||||
// Basic authentication (skip for /public routes)
|
|
||||||
const basicAuthMiddleware = basicAuth({
|
|
||||||
users: BASIC_AUTH_USERS,
|
|
||||||
challenge: true,
|
|
||||||
realm: "Voice Assistant",
|
|
||||||
});
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
if (agentMcpBearerToken && req.path.startsWith("/mcp/agents")) {
|
|
||||||
const authHeader = req.header("authorization") ?? "";
|
|
||||||
if (authHeader.startsWith("Bearer ")) {
|
|
||||||
const token = authHeader.slice("Bearer ".length).trim();
|
|
||||||
if (token === agentMcpBearerToken) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return basicAuthMiddleware(req, res, next);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Middleware
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
// Health check endpoint
|
|
||||||
app.get("/api/health", (_req, res) => {
|
|
||||||
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Conversation management endpoints
|
|
||||||
app.get("/api/conversations", async (_req, res) => {
|
|
||||||
try {
|
|
||||||
const conversations = await listConversations();
|
|
||||||
res.json(conversations);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[API] Failed to list conversations:", error);
|
|
||||||
res.status(500).json({ error: "Failed to list conversations" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.delete("/api/conversations/:id", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { id } = req.params;
|
|
||||||
await deleteConversation(id);
|
|
||||||
res.json({ success: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[API] Failed to delete conversation:", error);
|
|
||||||
res.status(500).json({ error: "Failed to delete conversation" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const port = resolvePaseoPort();
|
const daemonConfig = buildPaseoDaemonConfigFromEnv();
|
||||||
const agentMcpRoute = "/mcp/agents";
|
const daemon = await createPaseoDaemon(daemonConfig);
|
||||||
const agentMcpUrl = `http://127.0.0.1:${port}${agentMcpRoute}`;
|
|
||||||
const [agentMcpUser, agentMcpPassword] =
|
|
||||||
Object.entries(BASIC_AUTH_USERS)[0] ?? [];
|
|
||||||
const agentMcpAuthHeader =
|
|
||||||
agentMcpUser && agentMcpPassword
|
|
||||||
? `Basic ${Buffer.from(
|
|
||||||
`${agentMcpUser}:${agentMcpPassword}`
|
|
||||||
).toString("base64")}`
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const app = createServer();
|
daemon.httpServer.listen(daemonConfig.port, () => {
|
||||||
const httpServer = createHTTPServer(app);
|
|
||||||
|
|
||||||
// Initialize global agent manager + registry
|
|
||||||
const agentRegistry = new AgentRegistry();
|
|
||||||
const agentManager = new AgentManager({
|
|
||||||
clients: {
|
|
||||||
claude: new ClaudeAgentClient(),
|
|
||||||
codex: new CodexAgentClient(),
|
|
||||||
},
|
|
||||||
registry: agentRegistry,
|
|
||||||
agentControlMcp: {
|
|
||||||
url: agentMcpUrl,
|
|
||||||
...(agentMcpAuthHeader
|
|
||||||
? { headers: { Authorization: agentMcpAuthHeader } }
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
attachAgentRegistryPersistence(agentManager, agentRegistry);
|
|
||||||
const persistedRecords = await agentRegistry.list();
|
|
||||||
console.log(
|
|
||||||
`✓ Agent registry loaded (${persistedRecords.length} record${
|
|
||||||
persistedRecords.length === 1 ? "" : "s"
|
|
||||||
}); agents will initialize on demand`
|
|
||||||
);
|
|
||||||
|
|
||||||
const agentMcpTransports: AgentMcpTransportMap = new Map();
|
|
||||||
|
|
||||||
const createAgentMcpTransport = async (callerAgentId?: string) => {
|
|
||||||
// Create a NEW McpServer instance per session (not shared across sessions)
|
|
||||||
// Pass the caller agent ID so create_agent can auto-set parentAgentId
|
|
||||||
const agentMcpServer = await createAgentMcpServer({
|
|
||||||
agentManager,
|
|
||||||
agentRegistry,
|
|
||||||
callerAgentId,
|
|
||||||
});
|
|
||||||
|
|
||||||
const transport = new StreamableHTTPServerTransport({
|
|
||||||
sessionIdGenerator: () => randomUUID(),
|
|
||||||
onsessioninitialized: (sessionId) => {
|
|
||||||
agentMcpTransports.set(sessionId, transport);
|
|
||||||
console.log(`[Agent MCP] Session initialized: ${sessionId}`);
|
|
||||||
},
|
|
||||||
onsessionclosed: (sessionId) => {
|
|
||||||
agentMcpTransports.delete(sessionId);
|
|
||||||
console.log(`[Agent MCP] Session closed: ${sessionId}`);
|
|
||||||
},
|
|
||||||
enableDnsRebindingProtection: true,
|
|
||||||
allowedHosts: [`127.0.0.1:${port}`, `localhost:${port}`],
|
|
||||||
});
|
|
||||||
transport.onclose = () => {
|
|
||||||
if (transport.sessionId) {
|
|
||||||
agentMcpTransports.delete(transport.sessionId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
transport.onerror = (error) => {
|
|
||||||
console.error("[Agent MCP] Transport error:", error);
|
|
||||||
};
|
|
||||||
|
|
||||||
await agentMcpServer.connect(transport);
|
|
||||||
|
|
||||||
return transport;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAgentMcpRequest: express.RequestHandler = async (
|
|
||||||
req,
|
|
||||||
res
|
|
||||||
) => {
|
|
||||||
if (process.env.MCP_DEBUG === "1") {
|
|
||||||
console.log("[Agent MCP] request", {
|
|
||||||
method: req.method,
|
|
||||||
url: req.originalUrl,
|
|
||||||
sessionId: req.header("mcp-session-id"),
|
|
||||||
authorization: req.header("authorization"),
|
|
||||||
body: req.body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const sessionId = req.header("mcp-session-id");
|
|
||||||
let transport = sessionId
|
|
||||||
? agentMcpTransports.get(sessionId)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (!transport) {
|
|
||||||
if (req.method !== "POST") {
|
|
||||||
res.status(400).json({
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
error: {
|
|
||||||
code: -32000,
|
|
||||||
message: "Missing or invalid MCP session",
|
|
||||||
},
|
|
||||||
id: null,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isInitializeRequest(req.body)) {
|
|
||||||
res.status(400).json({
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
error: {
|
|
||||||
code: -32000,
|
|
||||||
message: "Initialization request expected",
|
|
||||||
},
|
|
||||||
id: null,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Extract optional caller agent ID from query string (sent by agents when connecting)
|
|
||||||
const callerAgentIdRaw = req.query.callerAgentId;
|
|
||||||
const callerAgentId =
|
|
||||||
typeof callerAgentIdRaw === "string"
|
|
||||||
? callerAgentIdRaw
|
|
||||||
: Array.isArray(callerAgentIdRaw) &&
|
|
||||||
typeof callerAgentIdRaw[0] === "string"
|
|
||||||
? callerAgentIdRaw[0]
|
|
||||||
: undefined;
|
|
||||||
transport = await createAgentMcpTransport(callerAgentId);
|
|
||||||
}
|
|
||||||
|
|
||||||
await transport.handleRequest(
|
|
||||||
req as any,
|
|
||||||
res as any,
|
|
||||||
req.body
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Agent MCP] Failed to handle request:", error);
|
|
||||||
if (!res.headersSent) {
|
|
||||||
res.status(500).json({
|
|
||||||
jsonrpc: "2.0",
|
|
||||||
error: {
|
|
||||||
code: -32603,
|
|
||||||
message: "Internal MCP server error",
|
|
||||||
},
|
|
||||||
id: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
app.post(agentMcpRoute, handleAgentMcpRequest);
|
|
||||||
app.get(agentMcpRoute, handleAgentMcpRequest);
|
|
||||||
app.delete(agentMcpRoute, handleAgentMcpRequest);
|
|
||||||
console.log(`✓ Agent MCP server mounted at ${agentMcpRoute}`);
|
|
||||||
|
|
||||||
// Initialize WebSocket server
|
|
||||||
const wsServer = new VoiceAssistantWebSocketServer(
|
|
||||||
httpServer,
|
|
||||||
agentManager,
|
|
||||||
agentRegistry,
|
|
||||||
{
|
|
||||||
agentMcpUrl,
|
|
||||||
agentMcpHeaders: agentMcpAuthHeader
|
|
||||||
? {
|
|
||||||
Authorization: agentMcpAuthHeader,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Initialize OpenAI client
|
|
||||||
const apiKey = process.env.OPENAI_API_KEY;
|
|
||||||
if (apiKey) {
|
|
||||||
console.log("✓ OpenAI client initialized");
|
|
||||||
|
|
||||||
// Initialize STT (Whisper)
|
|
||||||
const sttConfidenceThreshold = process.env.STT_CONFIDENCE_THRESHOLD
|
|
||||||
? parseFloat(process.env.STT_CONFIDENCE_THRESHOLD)
|
|
||||||
: undefined; // Will default to -3.0 in stt-openai.ts
|
|
||||||
|
|
||||||
const sttModel = process.env.STT_MODEL as STTConfig["model"];
|
|
||||||
|
|
||||||
initializeSTT({
|
|
||||||
apiKey,
|
|
||||||
confidenceThreshold: sttConfidenceThreshold,
|
|
||||||
...(sttModel ? { model: sttModel } : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize TTS
|
|
||||||
const ttsVoice = (process.env.TTS_VOICE || "alloy") as
|
|
||||||
| "alloy"
|
|
||||||
| "echo"
|
|
||||||
| "fable"
|
|
||||||
| "onyx"
|
|
||||||
| "nova"
|
|
||||||
| "shimmer";
|
|
||||||
const ttsModel = (process.env.TTS_MODEL || "tts-1") as "tts-1" | "tts-1-hd";
|
|
||||||
initializeTTS({
|
|
||||||
apiKey,
|
|
||||||
voice: ttsVoice,
|
|
||||||
model: ttsModel,
|
|
||||||
responseFormat: "pcm",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize agent title generator
|
|
||||||
initializeTitleGenerator(apiKey);
|
|
||||||
} else {
|
|
||||||
console.warn(
|
|
||||||
"⚠ OPENAI_API_KEY not set - LLM, STT, and TTS features will not work"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
httpServer.listen(port, () => {
|
|
||||||
console.log(
|
console.log(
|
||||||
`\n✓ Voice Assistant server running on http://localhost:${port}`
|
`\n✓ Voice Assistant server running on http://localhost:${daemonConfig.port}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Graceful shutdown
|
|
||||||
const handleShutdown = async (signal: string) => {
|
const handleShutdown = async (signal: string) => {
|
||||||
console.log(`\n${signal} received, shutting down gracefully...`);
|
console.log(`\n${signal} received, shutting down gracefully...`);
|
||||||
|
|
||||||
// Wait for agents to finish work
|
const forceExit = setTimeout(() => {
|
||||||
await closeAllAgents(agentManager);
|
|
||||||
|
|
||||||
// Close WebSocket and HTTP servers
|
|
||||||
wsServer.close();
|
|
||||||
httpServer.close(() => {
|
|
||||||
console.log("Server closed");
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Force exit after 10 seconds if HTTP server doesn't close
|
|
||||||
// This runs AFTER agent shutdown completes
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log("Forcing shutdown - HTTP server didn't close in time");
|
console.log("Forcing shutdown - HTTP server didn't close in time");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await daemon.close();
|
||||||
|
clearTimeout(forceExit);
|
||||||
|
console.log("Server closed");
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
clearTimeout(forceExit);
|
||||||
|
console.error("Shutdown failed:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on("SIGTERM", () => handleShutdown("SIGTERM"));
|
process.on("SIGTERM", () => handleShutdown("SIGTERM"));
|
||||||
process.on("SIGINT", () => handleShutdown("SIGINT"));
|
process.on("SIGINT", () => handleShutdown("SIGINT"));
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main().catch((error) => {
|
||||||
|
console.error("Failed to start server:", error);
|
||||||
async function closeAllAgents(agentManager: AgentManager): Promise<void> {
|
process.exit(1);
|
||||||
const agents = agentManager.listAgents();
|
});
|
||||||
for (const agent of agents) {
|
|
||||||
try {
|
|
||||||
await agentManager.closeAgent(agent.id);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[Agents] Failed to close agent ${agent.id}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All agents have been asked to stop; let the caller finish shutdown
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user