@agentforce/adk
Version:
AgentForce Agent Development Kit - A powerful framework for building AI agents and servers
356 lines (317 loc) • 12.9 kB
text/typescript
import { Hono } from "hono";
import type { AgentForceServer } from "../../../server";
import { logger as loggerMiddleware } from "hono/logger";
import { createAgentRouteHandler } from "../addRouteAgent";
import { createStaticRouteHandler } from "../addRoute";
import { createOpenAICompatibleRouteHandler } from "../useOpenAICompatibleRouting";
import { createOllamaGenerateRouteHandler, createOllamaChatRouteHandler } from "../useOllamaCompatibleRouting";
/**
* Starts an HTTP server for the AgentForceServer (terminal method)
* Compatible with Bun, Node.js, and Deno runtimes
* @param this - The AgentForceServer instance (bound context)
* @param host - The host address to bind the server to (default: "localhost")
* @param port - The port number to listen on (default: 3000)
* @returns {Promise<void>} This is a terminal method that starts the server - does not return the server instance
*/
export async function serve(this: AgentForceServer, host: string = "0.0.0.0", port: number = 3000): Promise<void> {
// Validate inputs
if (!host || typeof host !== "string") {
throw new Error("Host must be a non-empty string");
}
if (typeof port !== "number" || port <= 0 || port > 65535) {
throw new Error("Port must be a valid number between 1 and 65535");
}
// Get server information for logging
const serverName = this.getName();
const log = this.getLogger();
log.info({
serverName,
host,
port,
action: "server_starting",
}, `Starting server: ${serverName}`);
console.log(`Starting server: ${serverName}`);
console.log(`Server will bind to: ${host}:${port}`);
// Custom logger function for Hono middleware that parses HTTP request info
const customLogger = (message: string, ...rest: string[]): void => {
// Parse the message format: "--> GET /path \u001b[32m200\u001b[0m 12ms"
const httpLogPattern = /^--> (\w+) (.+?) (?:\u001b\[\d+m)?(\d+)(?:\u001b\[\d+m)? (\d+)ms$/;
const match = message.match(httpLogPattern);
if (match && match.length >= 5) {
const [, method, route, statusCode, duration] = match;
log.info({
type: "http_request",
method,
route,
statusCode: parseInt(statusCode || "0"),
duration: parseInt(duration || "0"),
durationUnit: "ms",
});
} else {
// Fallback for non-HTTP log messages
log.info(message, ...rest);
}
};
// Create Hono app with logger middleware
const app = new Hono();
app.use(loggerMiddleware(customLogger));
// Check for existing static routes to avoid conflicts
const staticRoutes = this.getStaticRoutes();
const existingRoutes = new Set(staticRoutes.map(route => `${route.method}:${route.path}`));
// Default route
app.get("/", (c) => {
return c.json({
status: "ok",
server: serverName,
timestamp: new Date().toISOString(),
version: "1.0.0",
});
});
// Health check route (only if not overridden by static route)
if (!existingRoutes.has("GET:/health")) {
app.get("/health", (c) => {
return c.json({
status: "healthy",
server: serverName,
timestamp: new Date().toISOString(),
});
});
}
// Add dynamic route agents
const routeAgents = this.getRouteAgents();
log.info({
serverName,
routeAgentsCount: routeAgents.length,
action: "registering_route_agents",
}, `Registering ${routeAgents.length} route agents`);
routeAgents.forEach(routeAgent => {
const { method, path, agent, schema } = routeAgent;
// Determine route type and create appropriate handler
let handler;
let routeType: string;
if (path === "/v1/chat/completions") {
// OpenAI-compatible route
handler = createOpenAICompatibleRouteHandler(agent, path);
routeType = "OpenAI-compatible";
} else if (path === "/api/generate") {
// Ollama Generate route
handler = createOllamaGenerateRouteHandler(agent, path);
routeType = "Ollama-compatible (generate)";
} else if (path === "/api/chat") {
// Ollama Chat route
handler = createOllamaChatRouteHandler(agent, path);
routeType = "Ollama-compatible (chat)";
} else {
// Legacy route with schema support
handler = createAgentRouteHandler(agent, method, path, schema);
routeType = schema ? "custom (with schema)" : "legacy";
}
// Register the route based on HTTP method
switch (method) {
case "GET":
app.get(path, handler);
break;
case "POST":
app.post(path, handler);
break;
case "PUT":
app.put(path, handler);
break;
case "DELETE":
app.delete(path, handler);
break;
case "PATCH":
app.patch(path, handler);
break;
case "HEAD":
// Hono doesn't have a dedicated head method, use all()
app.all(path, (c) => {
if (c.req.method === "HEAD") {
return handler(c);
}
return c.notFound();
});
break;
case "OPTIONS":
app.options(path, handler);
break;
default:
log.warn({
serverName,
method,
path,
action: "unsupported_method",
}, `Unsupported HTTP method: ${method} for path: ${path}`);
}
log.info({
serverName,
method,
path,
agentName: agent["getName"](),
routeType,
action: "route_registered",
}, `Registered ${routeType} route: ${method} ${path}`);
});
// Add static routes
log.info({
serverName,
staticRoutesCount: staticRoutes.length,
action: "registering_static_routes",
}, `Registering ${staticRoutes.length} static routes`);
staticRoutes.forEach(staticRoute => {
const { method, path, responseData } = staticRoute;
// Create static route handler
const handler = createStaticRouteHandler(responseData, method, path);
// Register the route based on HTTP method
switch (method) {
case "GET":
app.get(path, handler);
break;
case "POST":
app.post(path, handler);
break;
case "PUT":
app.put(path, handler);
break;
case "DELETE":
app.delete(path, handler);
break;
case "PATCH":
app.patch(path, handler);
break;
case "HEAD":
// Hono doesn't have a dedicated head method, use all()
app.all(path, (c) => {
if (c.req.method === "HEAD") {
return handler(c);
}
return c.notFound();
});
break;
case "OPTIONS":
app.options(path, handler);
break;
default:
log.warn({
serverName,
method,
path,
action: "unsupported_method",
}, `Unsupported HTTP method: ${method} for path: ${path}`);
}
log.info({
serverName,
method,
path,
routeType: "static",
action: "route_registered",
}, `Registered static route: ${method} ${path}`);
});
// Start the server using runtime-appropriate method
// Runtime detection and server startup
try {
if (typeof globalThis.Bun !== "undefined") {
// Bun runtime
const server = (globalThis as any).Bun.serve({
hostname: host,
port: port,
fetch: app.fetch,
idleTimeout: 120, // Set timeout to 120 seconds (2 minutes) for AI agent responses
});
log.info({
serverName,
host: server.hostname,
port: server.port,
runtime: "bun",
action: "server_started",
}, `🚀 Server running at http://${server.hostname}:${server.port}`);
console.log(`🚀 Server running at http://${server.hostname}:${server.port}`);
} else if (typeof (globalThis as any).Deno !== "undefined") {
// Deno runtime
(globalThis as any).Deno.serve({
hostname: host,
port: port,
}, app.fetch);
log.info({
serverName,
host,
port,
runtime: "deno",
action: "server_started",
}, `🚀 Server running at http://${host}:${port}`);
console.log(`🚀 Server running at http://${host}:${port}`);
} else {
// Node.js runtime - use Node.js built-in http module
const http = await import("node:http");
const server = http.createServer(async (req, res) => {
// Convert Node.js request to Web API Request
const url = `http://${host}:${port}${req.url || "/"}`;
const headers: Record<string, string> = {};
// Copy headers from Node.js request
for (const [key, value] of Object.entries(req.headers)) {
if (typeof value === "string") {
headers[key] = value;
} else if (Array.isArray(value)) {
headers[key] = value.join(", ");
}
}
// Handle request body for POST/PUT/PATCH
let body: string | undefined;
if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(chunk);
}
body = Buffer.concat(chunks).toString();
}
// Create Web API Request
const request = new Request(url, {
method: req.method || "GET",
headers,
body: body,
});
try {
// Process request with Hono app
const response = await app.fetch(request);
// Set response status and headers
res.statusCode = response.status;
response.headers.forEach((value, key) => {
res.setHeader(key, value);
});
// Send response body
if (response.body) {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
res.write(value);
}
}
res.end();
} catch (error) {
console.error("Error processing request:", error);
res.statusCode = 500;
res.end("Internal Server Error");
}
});
server.listen(port, host, () => {
log.info({
serverName,
host,
port,
runtime: "node",
action: "server_started",
}, `🚀 Server running at http://${host}:${port}`);
console.log(`🚀 Server running at http://${host}:${port}`);
});
}
} catch (error) {
log.error({
serverName,
error: error instanceof Error ? error.message : String(error),
action: "server_start_failed",
}, "Failed to start server");
return;
}
// Terminal method - does not return the server instance (server runs indefinitely)
}