/** * useOrchestratorCommands hook * * Parses chat messages for `/command` prefixes and routes them to the * orchestrator proxy API routes instead of the LLM. * * Supported commands: * /status — GET /api/orchestrator/health * /agents — GET /api/orchestrator/agents * /jobs — GET /api/orchestrator/queue/stats * /queue — alias for /jobs * /pause — POST /api/orchestrator/queue/pause * /resume — POST /api/orchestrator/queue/resume * /help — Display available commands locally (no API call) */ import { useCallback } from "react"; import type { Message } from "@/hooks/useChat"; // --------------------------------------------------------------------------- // Command definitions // --------------------------------------------------------------------------- export interface OrchestratorCommand { name: string; description: string; aliases?: string[]; } export const ORCHESTRATOR_COMMANDS: OrchestratorCommand[] = [ { name: "/status", description: "Show orchestrator health and status" }, { name: "/agents", description: "List all running agents" }, { name: "/jobs", description: "Show queue statistics", aliases: ["/queue"] }, { name: "/pause", description: "Pause the job queue" }, { name: "/resume", description: "Resume the job queue" }, { name: "/help", description: "Show available commands" }, ]; // --------------------------------------------------------------------------- // API response shapes (loosely typed — orchestrator may vary) // --------------------------------------------------------------------------- interface HealthResponse { status?: string; version?: string; uptime?: number; ready?: boolean; error?: string; } interface Agent { id?: string; sessionKey?: string; status?: string; type?: string; agentStatus?: string; startedAt?: string; label?: string; channel?: string; } interface AgentsResponse { agents?: Agent[]; error?: string; } interface QueueStats { pending?: number; active?: number; completed?: number; failed?: number; waiting?: number; delayed?: number; paused?: boolean; error?: string; } interface ActionResponse { success?: boolean; message?: string; status?: string; error?: string; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function makeId(): string { return `orch-${Date.now().toString()}-${Math.random().toString(36).slice(2, 8)}`; } function makeMessage(content: string): Message { return { id: makeId(), role: "assistant", content, createdAt: new Date().toISOString(), }; } function errorMessage(command: string, detail: string): Message { return makeMessage( `**Error running \`${command}\`**\n\n${detail}\n\n_Check that the orchestrator is running and the API key is configured._` ); } // --------------------------------------------------------------------------- // Formatters // --------------------------------------------------------------------------- function formatStatus(data: HealthResponse): string { if (data.error) { return `**Orchestrator Status**\n\nStatus: Not reachable\n\nError: ${data.error}`; } const statusLabel = data.status ?? (data.ready === true ? "ready" : "unknown"); const isReady = statusLabel === "ready" || statusLabel === "ok" || statusLabel === "healthy" || data.ready === true; const badge = isReady ? "Ready" : "Not Ready"; const lines: string[] = [ `**Orchestrator Status**\n`, `| Field | Value |`, `|---|---|`, `| Status | **${badge}** |`, ]; if (data.version != null) { lines.push(`| Version | \`${data.version}\` |`); } if (data.uptime != null) { const uptimeSec = Math.floor(data.uptime); const hours = Math.floor(uptimeSec / 3600); const mins = Math.floor((uptimeSec % 3600) / 60); const secs = uptimeSec % 60; const uptimeStr = hours > 0 ? `${String(hours)}h ${String(mins)}m ${String(secs)}s` : `${String(mins)}m ${String(secs)}s`; lines.push(`| Uptime | ${uptimeStr} |`); } return lines.join("\n"); } function formatAgents(raw: unknown): string { let agents: Agent[] = []; if (Array.isArray(raw)) { agents = raw as Agent[]; } else if (raw !== null && typeof raw === "object") { const obj = raw as AgentsResponse; if (obj.error) { return `**Agents**\n\nError: ${obj.error}`; } if (Array.isArray(obj.agents)) { agents = obj.agents; } } if (agents.length === 0) { return "**Agents**\n\nNo agents currently running."; } const lines: string[] = [ `**Agents** (${String(agents.length)} total)\n`, `| ID / Key | Status | Type / Channel | Started |`, `|---|---|---|---|`, ]; for (const agent of agents) { const id = agent.id ?? agent.sessionKey ?? "—"; const status = agent.agentStatus ?? agent.status ?? "—"; const type = agent.type ?? agent.channel ?? "—"; const started = agent.startedAt ? new Date(agent.startedAt).toLocaleString() : "—"; lines.push(`| \`${id}\` | ${status} | ${type} | ${started} |`); } return lines.join("\n"); } function formatQueueStats(data: QueueStats): string { if (data.error) { return `**Queue Stats**\n\nError: ${data.error}`; } const lines: string[] = [`**Queue Statistics**\n`, `| Metric | Count |`, `|---|---|`]; const metrics: [string, number | undefined][] = [ ["Pending", data.pending ?? data.waiting], ["Active", data.active], ["Delayed", data.delayed], ["Completed", data.completed], ["Failed", data.failed], ]; for (const [label, value] of metrics) { if (value !== undefined) { lines.push(`| ${label} | ${String(value)} |`); } } if (data.paused === true) { lines.push("\n_Queue is currently **paused**._"); } return lines.join("\n"); } function formatAction(command: string, data: ActionResponse): string { if (data.error) { return `**${command}** failed.\n\nError: ${data.error}`; } const verb = command === "/pause" ? "paused" : "resumed"; const msg = data.message ?? data.status ?? `Queue ${verb} successfully.`; return `**Queue ${verb}**\n\n${msg}`; } function formatHelp(): string { const lines: string[] = [ "**Available Orchestrator Commands**\n", "| Command | Description |", "|---|---|", ]; for (const cmd of ORCHESTRATOR_COMMANDS) { const name = cmd.aliases ? `${cmd.name} (${cmd.aliases.join(", ")})` : cmd.name; lines.push(`| \`${name}\` | ${cmd.description} |`); } lines.push("\n_Commands starting with `/` are routed to the orchestrator instead of the LLM._"); return lines.join("\n"); } // --------------------------------------------------------------------------- // Command parser // --------------------------------------------------------------------------- function parseCommandName(content: string): string | null { const trimmed = content.trim(); if (!trimmed.startsWith("/")) { return null; } const parts = trimmed.split(/\s+/); return parts[0]?.toLowerCase() ?? null; } // --------------------------------------------------------------------------- // Hook // --------------------------------------------------------------------------- export interface UseOrchestratorCommandsReturn { /** * Returns true if the content looks like an orchestrator command. */ isCommand: (content: string) => boolean; /** * Execute an orchestrator command. * Returns a Message with formatted markdown output, or null if not a command. */ executeCommand: (content: string) => Promise; } export function useOrchestratorCommands(): UseOrchestratorCommandsReturn { const isCommand = useCallback((content: string): boolean => { return content.trim().startsWith("/"); }, []); const executeCommand = useCallback(async (content: string): Promise => { const command = parseCommandName(content); if (!command) { return null; } // /help — local, no network if (command === "/help") { return makeMessage(formatHelp()); } // /status if (command === "/status") { try { const res = await fetch("/api/orchestrator/health", { method: "GET" }); const data = (await res.json()) as HealthResponse; return makeMessage(formatStatus(data)); } catch (err) { const detail = err instanceof Error ? err.message : "Network error"; return errorMessage("/status", detail); } } // /agents if (command === "/agents") { try { const res = await fetch("/api/orchestrator/agents", { method: "GET" }); const data: unknown = await res.json(); return makeMessage(formatAgents(data)); } catch (err) { const detail = err instanceof Error ? err.message : "Network error"; return errorMessage("/agents", detail); } } // /jobs or /queue if (command === "/jobs" || command === "/queue") { try { const res = await fetch("/api/orchestrator/queue/stats", { method: "GET" }); const data = (await res.json()) as QueueStats; return makeMessage(formatQueueStats(data)); } catch (err) { const detail = err instanceof Error ? err.message : "Network error"; return errorMessage(command, detail); } } // /pause if (command === "/pause") { try { const res = await fetch("/api/orchestrator/queue/pause", { method: "POST" }); const data = (await res.json()) as ActionResponse; return makeMessage(formatAction("/pause", data)); } catch (err) { const detail = err instanceof Error ? err.message : "Network error"; return errorMessage("/pause", detail); } } // /resume if (command === "/resume") { try { const res = await fetch("/api/orchestrator/queue/resume", { method: "POST" }); const data = (await res.json()) as ActionResponse; return makeMessage(formatAction("/resume", data)); } catch (err) { const detail = err instanceof Error ? err.message : "Network error"; return errorMessage("/resume", detail); } } // Unknown command — show help hint return makeMessage( `Unknown command: \`${command}\`\n\nType \`/help\` to see available commands.` ); }, []); return { isCommand, executeCommand }; }