import { NextRequest, NextResponse } from "next/server"; import { CopilotRuntime, ExperimentalEmptyAdapter, copilotRuntimeNextJSAppRouterEndpoint, } from "@copilotkit/runtime"; import { LangGraphAgent } from "@copilotkit/runtime/langgraph"; import crypto from "node:crypto"; // Emit a structured server-side error log with a correlation id so the // 500 we return to the client carries no stack/message details (which // can leak internal config, prompts, or upstream URLs) while operators // can still grep logs for the same `errorId` to find the full failure. function logRouteError(err: unknown): string { const error = err instanceof Error ? err : new Error(String(err)); const errorId = crypto.randomUUID(); console.error( JSON.stringify({ at: new Date().toISOString(), level: "error", phase: "setup", errorId, message: error.message, stack: error.stack, }), ); return errorId; } const AGENT_URL = process.env.AGENT_URL || "http://localhost:8123"; console.log("[copilotkit/route] Initializing CopilotKit runtime"); console.log(`[copilotkit/route] AGENT_URL: ${AGENT_URL}`); // Per-request request/response logging is gated behind this flag (default off). // Under d6 probe fan-out, unconditional per-request logs flooded Railway's // 500-logs/sec cap and killed the replica ("Messages dropped" → container stop). // Set SHOWCASE_ROUTE_DEBUG=1 to re-enable verbose per-request tracing locally. const ROUTE_DEBUG = process.env.SHOWCASE_ROUTE_DEBUG === "1" || process.env.SHOWCASE_ROUTE_DEBUG === "true"; function createAgent(graphId: string = "sample_agent") { return new LangGraphAgent({ deploymentUrl: `${AGENT_URL}/`, graphId, }); } const agentNames = [ "agentic_chat", "human_in_the_loop", "gen-ui-tool-based", "gen-ui-agent", "shared-state-read", "shared-state-write", "shared-state-streaming", "prebuilt-sidebar", "prebuilt-popup", "chat-slots", "chat-customization-css", "headless-simple", ]; const agents: Record = {}; for (const name of agentNames) { agents[name] = createAgent(); } // Dedicated-graph agents for tool-rendering + reasoning demos. agents["tool-rendering"] = createAgent("tool_rendering"); agents["tool-rendering-default-catchall"] = createAgent("tool_rendering"); agents["tool-rendering-custom-catchall"] = createAgent("tool_rendering"); agents["tool-rendering-reasoning-chain"] = createAgent( "tool_rendering_reasoning_chain", ); agents["agentic-chat-reasoning"] = createAgent("reasoning_agent"); agents["reasoning-default-render"] = createAgent("reasoning_agent"); // Interrupt variants — share the dedicated `interrupt_agent` graph that uses // langgraph's `interrupt()` primitive inside `schedule_meeting`. agents["gen-ui-interrupt"] = createAgent("interrupt_agent"); agents["interrupt-headless"] = createAgent("interrupt_agent"); // Dedicated-graph agents — each cell has its own LangGraph graph with a // tailored system prompt (tools=[], CopilotKitMiddleware attached). agents["frontend_tools"] = createAgent("frontend_tools"); agents["frontend-tools-async"] = createAgent("frontend_tools_async"); agents["hitl-in-app"] = createAgent("hitl_in_app"); // In-Chat HITL via the high-level `useHumanInTheLoop` hook — backend // agent has zero tools; the frontend-registered `book_call` tool is // injected into the LLM's tool list by `CopilotKitMiddleware`. Both // the canonical `hitl-in-chat` demo and the `hitl-in-chat-booking` // alias share the same backend graph. agents["hitl-in-chat"] = createAgent("hitl_in_chat"); agents["hitl-in-chat-booking"] = createAgent("hitl_in_chat"); // HITL step-selection: dedicated graph with tools=[] + CopilotKitMiddleware. // The `human_in_the_loop` alias in the neutral-assistant loop above maps to // `sample_agent` which has 7+ backend tools and a custom AgentState — the // frontend-only `generate_task_steps` tool (from useHumanInTheLoop) is the // ONLY tool this demo needs, so a minimal graph avoids state/tool contention. agents["human_in_the_loop"] = createAgent("hitl_steps"); agents["readonly-state-agent-context"] = createAgent( "readonly_state_agent_context", ); // Shared State (Read + Write) — bidirectional shared state between UI and // agent. UI writes `preferences` via agent.setState; middleware reads them // into the system prompt; agent writes `notes` back via the `set_notes` tool. agents["shared-state-read-write"] = createAgent("shared_state_read_write"); // Sub-Agents — supervisor delegates to research_agent / writing_agent / // critique_agent (each a full create_agent under the hood). Every delegation // is appended to `state.delegations` for live UI rendering. agents["subagents"] = createAgent("subagents"); agents["default"] = createAgent(); console.log( `[copilotkit/route] Registered ${Object.keys(agents).length} agent names: ${Object.keys(agents).join(", ")}`, ); export const POST = async (req: NextRequest) => { try { const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({ endpoint: "/api/copilotkit", serviceAdapter: new ExperimentalEmptyAdapter(), runtime: new CopilotRuntime({ // @ts-ignore agents, }), }); const response = await handleRequest(req); if (!response.ok) { console.log(`[copilotkit/route] Response status: ${response.status}`); } else if (ROUTE_DEBUG) { console.log(`[copilotkit/route] Response status: ${response.status}`); } return response; } catch (error: unknown) { // Log full message + stack server-side under a correlation id; return // only the id to the client so we don't leak internal details (upstream // URLs, env-driven config, prompts, etc.) into HTTP responses. const errorId = logRouteError(error); return NextResponse.json( { error: "internal runtime error", errorId }, { status: 500 }, ); } }; export const GET = async () => { let agentStatus = "unknown"; try { const res = await fetch(`${AGENT_URL}/ok`, { signal: AbortSignal.timeout(3000), }); agentStatus = res.ok ? "reachable" : `error (${res.status})`; } catch (e: unknown) { agentStatus = `unreachable (${(e as Error).message})`; } return NextResponse.json({ status: "ok", agent_url: AGENT_URL, agent_status: agentStatus, }); };