/** * Unified Agents Page * * Two modes: * - Mesh disabled: Shows local (telemetry) agents only. No registration. * - Mesh enabled: Two sections — Local Agents (with register/unregister per agent) * and Mesh Registry (all registered agents with discovery). * * Route: /agents */ import React, { useState, useCallback, useMemo } from 'react'; import { useApi } from '../hooks/useApi'; import { useFeatures } from '../hooks/useFeatures'; import { getMeshAgents, registerMeshAgent, unregisterMeshAgent, discoverAgents, type MeshAgent, } from '../api/discovery'; import { getAgents } from '../api/agents'; import type { Agent as TelemetryAgent } from '../api/core'; import { listBudgets, getBudgetStatus, type CostBudgetStatusData } from '../api/budgets'; import { BudgetStatusBadge } from '../components/BudgetStatusBadge'; // ─── Helpers ──────────────────────────────────────────────── /** * Parse a timestamp that may be an ISO string OR an epoch-millis value — * including stringified floats like "1783015898521.0" that some ingest paths * (e.g. webhook events) produce. Returns NaN for anything unparseable. */ function parseTs(v: string | number | null | undefined): number { if (v == null) return NaN; if (typeof v === 'number') return v; const s = String(v).trim(); if (/^\d+(\.\d+)?$/.test(s)) return Math.round(Number(s)); // epoch millis return new Date(s).getTime(); } function timeAgo(iso: string): string { const t = parseTs(iso); if (!Number.isFinite(t)) return 'unknown'; const seconds = Math.floor((Date.now() - t) / 1000); if (seconds < 0) return 'just now'; if (seconds < 60) return `${seconds}s ago`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); if (days < 30) return `${days}d ago`; const months = Math.floor(days / 30); return `${months}mo ago`; } /** Human-readable date-time, or an em dash when the value is unparseable. */ function formatDateTime(v: string): string { const t = parseTs(v); return Number.isFinite(t) ? new Date(t).toLocaleString() : '—'; } function getInitials(name: string): string { return name .split(/[\s_-]+/) .map((w) => w[0]?.toUpperCase() ?? '') .slice(0, 2) .join(''); } const AGENT_COLORS = [ 'from-indigo-500 to-purple-500', 'from-emerald-500 to-teal-500', 'from-amber-500 to-orange-500', 'from-pink-500 to-rose-500', 'from-sky-500 to-blue-500', 'from-violet-500 to-fuchsia-500', ]; function colorForAgent(name: string): string { let hash = 0; for (let i = 0; i < name.length; i++) { hash = (hash * 31 + name.charCodeAt(i)) | 0; } return AGENT_COLORS[Math.abs(hash) % AGENT_COLORS.length] ?? AGENT_COLORS[0]; } function statusDot(lastSeen: string): { color: string; label: string } { const t = parseTs(lastSeen); if (!Number.isFinite(t)) return { color: 'bg-gray-400', label: 'Unknown' }; const minutes = (Date.now() - t) / 60_000; if (minutes < 5) return { color: 'bg-green-500', label: 'Online' }; if (minutes < 60) return { color: 'bg-yellow-500', label: 'Idle' }; return { color: 'bg-gray-400', label: 'Offline' }; } // ─── Local Agent Card ─────────────────────────────────────── function LocalAgentCard({ agent, meshEnabled, isRegistered, onRegister, onSelect, selected, budgetStatus, }: { agent: TelemetryAgent; meshEnabled: boolean; isRegistered: boolean; onRegister: () => void; onSelect: () => void; selected: boolean; budgetStatus?: CostBudgetStatusData; }): React.ReactElement { const status = statusDot(agent.lastSeenAt); return ( )} ); } // ─── Mesh Agent Card ──────────────────────────────────────── function MeshAgentCard({ agent, telemetry, onSelect, selected, onUnregister, }: { agent: MeshAgent; telemetry?: TelemetryAgent; onSelect: () => void; selected: boolean; onUnregister: () => void; }): React.ReactElement { const status = statusDot(agent.last_seen); return ( ); } // ─── Agent Detail Panel ───────────────────────────────────── function LocalAgentDetail({ agent, onClose, }: { agent: TelemetryAgent; onClose: () => void; }): React.ReactElement { const status = statusDot(agent.lastSeenAt); return (
{getInitials(agent.name)}

{agent.name}

{status.label}
{agent.description &&

{agent.description}

}

Sessions

{agent.sessionCount}

First Seen

{formatDateTime(agent.firstSeenAt)}

Last Seen

{formatDateTime(agent.lastSeenAt)}

{agent.pausedAt && (

Paused

{agent.pauseReason ?? 'By guardrail'}

)} {agent.modelOverride && (

Model Override

{agent.modelOverride}

)}
Tenant: {agent.tenantId}
); } function MeshAgentDetail({ agent, telemetry, onClose, onUnregister, }: { agent: MeshAgent; telemetry?: TelemetryAgent; onClose: () => void; onUnregister: () => void; }): React.ReactElement { const status = statusDot(agent.last_seen); return (
{getInitials(agent.name)}

{agent.name}

{status.label}

{agent.description}

Endpoint

{agent.endpoint}

Protocol

{agent.protocol}

Registered

{new Date(agent.registered_at).toLocaleString()}

{telemetry?.pausedAt && (

Paused

{telemetry.pauseReason ?? 'By guardrail'}

)}
{agent.capabilities.length > 0 && (

Capabilities

{agent.capabilities.map((cap) => ( {cap} ))}
)}
Last seen: {new Date(agent.last_seen).toLocaleString()}
); } // ─── Main Component ───────────────────────────────────────── export function Agents(): React.ReactElement { const { mesh: meshEnabled, loading: featuresLoading } = useFeatures(); const [search, setSearch] = useState(''); const [selectedLocal, setSelectedLocal] = useState(null); const [selectedMesh, setSelectedMesh] = useState(null); const [discoveryQuery, setDiscoveryQuery] = useState(''); const [discoveryResults, setDiscoveryResults] = useState< { agent: MeshAgent; score: number; matchedTerms: string[] }[] | null >(null); // Always fetch local (telemetry) agents const telemetryQuery = useApi(() => getAgents(), []); // Hide 'openclaw-brad' — it's a duplicate of 'main' from the OTel service name. // Events are being merged server-side but the agent keeps getting recreated. const telemetryAgents = (telemetryQuery.data ?? []).filter((a) => a.name !== 'openclaw-brad'); // Only fetch mesh agents when mesh is enabled const meshQuery = useApi(() => (meshEnabled ? getMeshAgents() : Promise.resolve([])), [meshEnabled]); const meshAgents = meshQuery.data ?? []; // Set of mesh-registered agent names (lowercase) for quick lookup const meshRegisteredNames = useMemo(() => { const set = new Set(); for (const m of meshAgents) set.add(m.name.toLowerCase()); return set; }, [meshAgents]); // Telemetry lookup by name for mesh agent enrichment const telemetryMap = useMemo(() => { const map = new Map(); for (const t of telemetryAgents) { map.set(t.name.toLowerCase(), t); const stripped = t.name.toLowerCase().replace(/^openclaw-/, ''); if (stripped !== t.name.toLowerCase()) map.set(stripped, t); } return map; }, [telemetryAgents]); // Budget statuses const [agentBudgetStatuses, setAgentBudgetStatuses] = React.useState>({}); React.useEffect(() => { listBudgets({ scope: 'agent', enabled: true }).then(({ budgets }) => { budgets.forEach((b) => { getBudgetStatus(b.id).then((status) => { setAgentBudgetStatuses((prev) => ({ ...prev, ...(b.agentId ? { [b.agentId]: status } : {}), })); }).catch(() => {}); }); }).catch(() => {}); }, []); // Filter local agents const filteredLocal = telemetryAgents.filter( (a) => !search || a.name.toLowerCase().includes(search.toLowerCase()) || (a.description ?? '').toLowerCase().includes(search.toLowerCase()), ); // Filter mesh agents (or discovery results) const meshToShow = discoveryResults ? discoveryResults.map((r) => r.agent) : meshAgents; const filteredMesh = meshToShow.filter( (a) => !search || a.name.toLowerCase().includes(search.toLowerCase()) || a.description.toLowerCase().includes(search.toLowerCase()) || a.capabilities.some((c) => c.toLowerCase().includes(search.toLowerCase())), ); const selectedLocalAgent = telemetryAgents.find((a) => a.name === selectedLocal) ?? null; const selectedMeshAgent = meshAgents.find((a) => a.name === selectedMesh) ?? null; const handleUnregister = useCallback( async (name: string) => { if (!confirm(`Unregister agent "${name}" from mesh?`)) return; await unregisterMeshAgent(name); if (selectedMesh === name) setSelectedMesh(null); meshQuery.refetch(); }, [meshQuery, selectedMesh], ); const handleRegisterLocal = useCallback( async (agent: TelemetryAgent) => { await registerMeshAgent({ name: agent.name, description: agent.description || '', capabilities: [], endpoint: '', }); meshQuery.refetch(); }, [meshQuery], ); const handleDiscover = async () => { if (!discoveryQuery.trim()) { setDiscoveryResults(null); return; } const resp = await discoverAgents({ query: discoveryQuery.trim() }); setDiscoveryResults(resp.results as { agent: MeshAgent; score: number; matchedTerms: string[] }[]); }; const loading = featuresLoading || telemetryQuery.loading; return (
{/* Header */}

Agents

{meshEnabled ? 'Manage local agents and mesh registry' : 'View agents observed through telemetry'}

{/* Search */}
setSearch(e.target.value)} data-testid="search-input" /> {meshEnabled && (
setDiscoveryQuery(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleDiscover()} /> {discoveryResults && ( )}
)}
{discoveryResults && (

{discoveryResults.length} result(s) for "{discoveryQuery}"

)}
{/* Loading */} {loading && (
Loading agents…
)} {/* Errors (non-fatal) */} {telemetryQuery.error && (
{telemetryQuery.error}
)} {/* ── Local Agent Detail ── */} {selectedLocalAgent && ( setSelectedLocal(null)} /> )} {/* ── Mesh Agent Detail ── */} {selectedMeshAgent && ( setSelectedMesh(null)} onUnregister={() => handleUnregister(selectedMeshAgent.name)} /> )} {/* ── Local Agents Section ── */} {!loading && (

{meshEnabled ? 'Local Agents' : 'Agents'}

{filteredLocal.length} agent{filteredLocal.length !== 1 ? 's' : ''}
{filteredLocal.length === 0 ? (
🤖

No agents found

Agents appear here once they start sending telemetry

) : (
{filteredLocal.map((agent) => ( handleRegisterLocal(agent)} onSelect={() => setSelectedLocal(selectedLocal === agent.name ? null : agent.name) } selected={selectedLocal === agent.name} budgetStatus={agentBudgetStatuses[agent.name]} /> ))}
)}
)} {/* ── Mesh Registry Section (only when mesh enabled) ── */} {meshEnabled && !loading && (

Mesh Registry

{filteredMesh.length} registered agent{filteredMesh.length !== 1 ? 's' : ''}
{meshQuery.error && (
Could not reach mesh service: {meshQuery.error}
)} {filteredMesh.length === 0 && !meshQuery.error ? (

No agents registered in mesh

Register local agents above to add them to the mesh

) : (
{filteredMesh.map((agent) => ( setSelectedMesh(selectedMesh === agent.name ? null : agent.name) } onUnregister={() => handleUnregister(agent.name)} /> ))}
)}
)}
); }