/**
* 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 (
{getInitials(agent.name)}
{agent.name}
{agent.description && (
{agent.description}
)}
{agent.capabilities.length > 0 && (
{agent.capabilities.map((cap) => (
{cap}
))}
)}
{agent.protocol || 'No protocol'}
{telemetry?.pausedAt && (
⏸ Paused
)}
Last seen {timeAgo(telemetry?.lastSeenAt ?? agent.last_seen)}
);
}
// ─── 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}
}
Close
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}
Unregister
Close
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 */}
{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)}
/>
))}
)}
)}
);
}