/** * GoClaw Gateway Proxy * * Forwards orchestrator/agent/tool requests from the Node.js tRPC server * to the Go Gateway running on :18789. * * The Go Gateway handles: * - LLM orchestration (tool-use loop) * - Tool execution (shell, file, docker, http) * - Agent listing from DB * - Model listing from LLM provider * * When GATEWAY_URL is not set or the gateway is unreachable, all functions * return a structured error so callers can fall back gracefully. */ const GATEWAY_BASE_URL = process.env.GATEWAY_URL ?? "http://localhost:18789"; const GATEWAY_TIMEOUT_MS = 180_000; // 3 min — LLM can be slow const QUICK_TIMEOUT_MS = 5_000; // ─── Types ──────────────────────────────────────────────────────────────────── export interface GatewayMessage { role: "user" | "assistant" | "system"; content: string; } export interface GatewayToolCallStep { tool: string; args: Record; result: unknown; // required — matches ToolCallStep interface in Chat.tsx / Skills.tsx error?: string; success: boolean; durationMs: number; } export interface GatewayChatResult { success: boolean; response: string; toolCalls: GatewayToolCallStep[]; model?: string; usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number; }; error?: string; } export interface GatewayOrchestratorConfig { id: number | null; name: string; model: string; temperature: number; maxTokens: number; allowedTools: string[]; systemPromptPreview: string; } export interface GatewayToolDef { name: string; description: string; parameters: Record; } export interface GatewayToolResult { success: boolean; output?: unknown; error?: string; durationMs: number; } export interface GatewayHealthResult { connected: boolean; latencyMs: number; llm?: { connected: boolean; latencyMs: number }; error?: string; } // ─── Health ─────────────────────────────────────────────────────────────────── /** * Check if the Go Gateway is running and healthy. * Also returns LLM provider health (Ollama / cloud API). */ export async function checkGatewayHealth(): Promise { const start = Date.now(); try { const res = await fetch(`${GATEWAY_BASE_URL}/health`, { signal: AbortSignal.timeout(QUICK_TIMEOUT_MS), }); const latencyMs = Date.now() - start; if (res.ok) { const data = await res.json(); return { connected: true, latencyMs, // Go Gateway may return "ollama" or "llm" key depending on version llm: data.llm ?? data.ollama, }; } return { connected: false, latencyMs, error: `HTTP ${res.status}` }; } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); return { connected: false, latencyMs: Date.now() - start, error: msg }; } } /** * Returns true if the Go Gateway is reachable (fast check, no LLM ping). */ export async function isGatewayAvailable(): Promise { try { const res = await fetch(`${GATEWAY_BASE_URL}/health`, { signal: AbortSignal.timeout(2_000), }); return res.ok; } catch { return false; } } // ─── Orchestrator ───────────────────────────────────────────────────────────── /** * Send a chat message to the Go Orchestrator. * Includes full tool-use loop — the Go side handles all LLM ↔ tool iterations. */ export async function gatewayChat( messages: GatewayMessage[], model?: string, maxIter?: number ): Promise { try { const res = await fetch(`${GATEWAY_BASE_URL}/api/orchestrator/chat`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages, model, maxIter }), signal: AbortSignal.timeout(GATEWAY_TIMEOUT_MS), }); if (!res.ok) { const text = await res.text(); return { success: false, response: "", toolCalls: [], error: `Gateway error (${res.status}): ${text}`, }; } return res.json(); } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); return { success: false, response: "", toolCalls: [], error: `Gateway unreachable: ${msg}. Is the Go Gateway running on ${GATEWAY_BASE_URL}?`, }; } } /** * Get orchestrator config from Go Gateway (reads from DB). */ export async function getGatewayOrchestratorConfig(): Promise { try { const res = await fetch(`${GATEWAY_BASE_URL}/api/orchestrator/config`, { signal: AbortSignal.timeout(QUICK_TIMEOUT_MS), }); if (!res.ok) return null; return res.json(); } catch { return null; } } // ─── Models ─────────────────────────────────────────────────────────────────── /** * Get list of models from Go Gateway (proxied from LLM provider). */ export async function getGatewayModels(): Promise<{ data: { id: string }[] } | null> { try { const res = await fetch(`${GATEWAY_BASE_URL}/api/models`, { signal: AbortSignal.timeout(10_000), }); if (!res.ok) return null; return res.json(); } catch { return null; } } // ─── Agents ─────────────────────────────────────────────────────────────────── /** * Get list of agents from Go Gateway (reads from DB). */ export async function getGatewayAgents(): Promise<{ agents: unknown[]; count: number } | null> { try { const res = await fetch(`${GATEWAY_BASE_URL}/api/agents`, { signal: AbortSignal.timeout(QUICK_TIMEOUT_MS), }); if (!res.ok) return null; return res.json(); } catch { return null; } } /** * Get a single agent by ID from Go Gateway. */ export async function getGatewayAgent(id: number): Promise { try { const res = await fetch(`${GATEWAY_BASE_URL}/api/agents/${id}`, { signal: AbortSignal.timeout(QUICK_TIMEOUT_MS), }); if (!res.ok) return null; return res.json(); } catch { return null; } } // ─── Tools ──────────────────────────────────────────────────────────────────── /** * Get list of available tools from Go Gateway. */ export async function getGatewayTools(): Promise { try { const res = await fetch(`${GATEWAY_BASE_URL}/api/tools`, { signal: AbortSignal.timeout(QUICK_TIMEOUT_MS), }); if (!res.ok) return null; const data = await res.json(); // Go returns OpenAI format: { tools: [{type: "function", function: {name, description, parameters}}, ...], count: N } if (!Array.isArray(data.tools)) return null; return data.tools.map((t: { type?: string; function?: { name: string; description: string; parameters: Record }; name?: string; description?: string; parameters?: Record }) => { // Handle OpenAI format: {type: "function", function: {name, ...}} if (t.function) { return { name: t.function.name, description: t.function.description, parameters: t.function.parameters, } as GatewayToolDef; } // Handle flat format: {name, description, parameters} return t as GatewayToolDef; }); } catch { return null; } } /** * Execute a single tool via Go Gateway. */ export async function executeGatewayTool( toolName: string, args: Record ): Promise { try { const res = await fetch(`${GATEWAY_BASE_URL}/api/tools/execute`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tool: toolName, args }), signal: AbortSignal.timeout(30_000), }); if (!res.ok) { const text = await res.text(); return { success: false, error: `Gateway error (${res.status}): ${text}`, durationMs: 0, }; } return res.json(); } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); return { success: false, error: `Gateway unreachable: ${msg}`, durationMs: 0, }; } } // ─── Nodes / Docker Swarm ───────────────────────────────────────────────────── export interface GatewayNodeInfo { id: string; hostname: string; role: string; status: string; availability: string; ip: string; os: string; arch: string; cpuCores: number; memTotalMB: number; dockerVersion: string; isLeader: boolean; managerAddr?: string; labels: Record; updatedAt: string; } export interface GatewayContainerInfo { id: string; name: string; image: string; state: string; status: string; } export interface GatewayNodesResult { nodes: GatewayNodeInfo[]; count: number; swarmActive: boolean; managers?: number; totalNodes?: number; containers?: GatewayContainerInfo[]; fetchedAt: string; } export interface GatewayContainerStat { id: string; name: string; cpuPct: number; memUseMB: number; memLimMB: number; memPct: number; } export interface GatewayNodeStatsResult { stats: GatewayContainerStat[]; count: number; fetchedAt: string; } /** * Get Docker Swarm nodes (or standalone Docker host) from Go Gateway. */ export async function getGatewayNodes(): Promise { try { const res = await fetch(`${GATEWAY_BASE_URL}/api/nodes`, { signal: AbortSignal.timeout(10_000), }); if (!res.ok) return null; return res.json(); } catch { return null; } } /** * Get live container CPU/RAM stats from Go Gateway. */ export async function getGatewayNodeStats(): Promise { try { const res = await fetch(`${GATEWAY_BASE_URL}/api/nodes/stats`, { signal: AbortSignal.timeout(15_000), }); if (!res.ok) return null; return res.json(); } catch { return null; } }