- Coordinator agent accepts complex tasks and decomposes into subtasks - Delegates to specialized worker agents via A2A v0.3 JSON-RPC - Handles x402 payment flows (paid skills return payment-required) - Free skills execute end-to-end through delegation chain - Web dashboard with real-time swarm monitoring - 14 tests passing - Worker agents: screenshot, PDF, HTML (via A2A gateway) - Event log tracks all delegations and completions
260 lines
12 KiB
HTML
260 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Agent Swarm - Multi-Agent Orchestration</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0f1117; color: #e1e4e8; min-height: 100vh; }
|
|
.header { background: linear-gradient(135deg, #1a1b26 0%, #24283b 100%); padding: 24px 32px; border-bottom: 1px solid #2d3148; }
|
|
.header h1 { font-size: 24px; font-weight: 700; color: #7aa2f7; display: flex; align-items: center; gap: 12px; }
|
|
.header h1 span { font-size: 14px; color: #565f89; font-weight: 400; }
|
|
.header p { color: #787c99; margin-top: 4px; font-size: 14px; }
|
|
|
|
.stats { display: flex; gap: 16px; padding: 20px 32px; flex-wrap: wrap; }
|
|
.stat { background: #1a1b26; border: 1px solid #2d3148; border-radius: 8px; padding: 16px 20px; min-width: 140px; flex: 1; }
|
|
.stat .label { font-size: 12px; color: #565f89; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
.stat .value { font-size: 28px; font-weight: 700; margin-top: 4px; }
|
|
.stat .value.blue { color: #7aa2f7; }
|
|
.stat .value.green { color: #9ece6a; }
|
|
.stat .value.yellow { color: #e0af68; }
|
|
.stat .value.red { color: #f7768e; }
|
|
|
|
.main { padding: 20px 32px; display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
|
|
@media (max-width: 900px) { .main { grid-template-columns: 1fr; } }
|
|
|
|
.panel { background: #1a1b26; border: 1px solid #2d3148; border-radius: 8px; overflow: hidden; }
|
|
.panel-header { padding: 14px 18px; border-bottom: 1px solid #2d3148; display: flex; justify-content: space-between; align-items: center; }
|
|
.panel-header h2 { font-size: 16px; font-weight: 600; }
|
|
.panel-body { padding: 18px; max-height: 400px; overflow-y: auto; }
|
|
|
|
.task-form { padding: 18px; border-bottom: 1px solid #2d3148; }
|
|
.task-form textarea { width: 100%; background: #16161e; border: 1px solid #2d3148; border-radius: 6px; padding: 10px; color: #e1e4e8; font-size: 14px; resize: vertical; min-height: 80px; font-family: inherit; }
|
|
.task-form textarea:focus { outline: none; border-color: #7aa2f7; }
|
|
.task-form button { margin-top: 10px; background: #7aa2f7; color: #1a1b26; border: none; padding: 8px 20px; border-radius: 6px; font-weight: 600; cursor: pointer; font-size: 14px; }
|
|
.task-form button:hover { background: #89b4fa; }
|
|
.task-form button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
.swarm-card { background: #16161e; border: 1px solid #2d3148; border-radius: 6px; padding: 14px; margin-bottom: 12px; }
|
|
.swarm-card .swarm-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
|
.swarm-card .instruction { font-size: 13px; color: #a9b1d6; margin-bottom: 10px; word-break: break-word; }
|
|
|
|
.badge { padding: 3px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; }
|
|
.badge.planned { background: #2d3148; color: #787c99; }
|
|
.badge.running { background: #1a3a1a; color: #9ece6a; }
|
|
.badge.completed { background: #1a2a3a; color: #7aa2f7; }
|
|
.badge.failed { background: #3a1a1a; color: #f7768e; }
|
|
|
|
.subtask { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: #1a1b26; border-radius: 4px; margin-top: 6px; font-size: 13px; }
|
|
.subtask .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
.subtask .dot.pending { background: #565f89; }
|
|
.subtask .dot.running { background: #e0af68; }
|
|
.subtask .dot.completed { background: #9ece6a; }
|
|
.subtask .dot.failed { background: #f7768e; }
|
|
|
|
.worker-card { display: flex; align-items: center; gap: 12px; padding: 12px; background: #16161e; border: 1px solid #2d3148; border-radius: 6px; margin-bottom: 8px; }
|
|
.worker-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 700; }
|
|
.worker-icon.screenshot { background: #1a3a2a; color: #9ece6a; }
|
|
.worker-icon.pdf { background: #3a1a2a; color: #f7768e; }
|
|
.worker-icon.html { background: #1a2a3a; color: #7aa2f7; }
|
|
.worker-info .name { font-weight: 600; font-size: 14px; }
|
|
.worker-info .skills { font-size: 12px; color: #565f89; }
|
|
.worker-cost { margin-left: auto; font-size: 13px; color: #e0af68; font-weight: 600; }
|
|
|
|
.event-row { display: flex; gap: 10px; padding: 6px 0; border-bottom: 1px solid #16161e; font-size: 12px; }
|
|
.event-time { color: #565f89; flex-shrink: 0; font-family: monospace; }
|
|
.event-action { color: #7aa2f7; flex-shrink: 0; min-width: 120px; }
|
|
.event-data { color: #a9b1d6; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
.arch-diagram { padding: 18px; text-align: center; }
|
|
.arch-diagram svg { max-width: 100%; }
|
|
.arch-flow { display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; padding: 12px; }
|
|
.arch-node { padding: 10px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; border: 2px solid; }
|
|
.arch-node.coord { background: #1a2a3a; border-color: #7aa2f7; color: #7aa2f7; }
|
|
.arch-node.worker { background: #1a3a2a; border-color: #9ece6a; color: #9ece6a; }
|
|
.arch-node.payment { background: #3a2a1a; border-color: #e0af68; color: #e0af68; }
|
|
.arch-arrow { color: #565f89; font-size: 20px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="header">
|
|
<h1>Agent Swarm <span>v1.0.0</span></h1>
|
|
<p>Multi-agent task decomposition and delegation via A2A protocol + x402 payments</p>
|
|
</div>
|
|
|
|
<div class="stats" id="stats">
|
|
<div class="stat"><div class="label">Total Swarms</div><div class="value blue" id="stat-total">0</div></div>
|
|
<div class="stat"><div class="label">Running</div><div class="value yellow" id="stat-running">0</div></div>
|
|
<div class="stat"><div class="label">Completed</div><div class="value green" id="stat-completed">0</div></div>
|
|
<div class="stat"><div class="label">Worker Agents</div><div class="value blue" id="stat-workers">0</div></div>
|
|
<div class="stat"><div class="label">Events</div><div class="value blue" id="stat-events">0</div></div>
|
|
</div>
|
|
|
|
<div class="main">
|
|
<!-- Left: Task submission + Swarms -->
|
|
<div>
|
|
<div class="panel">
|
|
<div class="panel-header"><h2>Submit Task</h2></div>
|
|
<div class="task-form">
|
|
<textarea id="taskInput" placeholder="Describe a complex task, e.g.: Create a visual report about opspawn.com including a screenshot and summary"></textarea>
|
|
<button id="submitBtn" onclick="submitTask()">Launch Swarm</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel" style="margin-top: 16px;">
|
|
<div class="panel-header"><h2>Active Swarms</h2></div>
|
|
<div class="panel-body" id="swarmList"><p style="color: #565f89;">No swarms yet. Submit a task above.</p></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right: Workers + Events + Architecture -->
|
|
<div>
|
|
<div class="panel">
|
|
<div class="panel-header"><h2>Architecture</h2></div>
|
|
<div class="arch-flow">
|
|
<div class="arch-node coord">Coordinator</div>
|
|
<div class="arch-arrow">→</div>
|
|
<div class="arch-node worker">A2A Workers</div>
|
|
<div class="arch-arrow">→</div>
|
|
<div class="arch-node payment">x402 Payment</div>
|
|
<div class="arch-arrow">→</div>
|
|
<div class="arch-node coord">Results</div>
|
|
</div>
|
|
<p style="padding: 0 18px 14px; font-size: 12px; color: #565f89; text-align: center;">
|
|
Complex task → Decompose into subtasks → Delegate to specialized agents via A2A → Settle payments via x402 → Assemble results
|
|
</p>
|
|
</div>
|
|
|
|
<div class="panel" style="margin-top: 16px;">
|
|
<div class="panel-header"><h2>Worker Agents</h2></div>
|
|
<div class="panel-body" id="workerList"></div>
|
|
</div>
|
|
|
|
<div class="panel" style="margin-top: 16px;">
|
|
<div class="panel-header"><h2>Event Log</h2></div>
|
|
<div class="panel-body" id="eventLog"><p style="color: #565f89;">No events yet.</p></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const BASE = window.location.origin;
|
|
|
|
async function fetchJSON(path) {
|
|
const res = await fetch(BASE + path);
|
|
return res.json();
|
|
}
|
|
|
|
async function submitTask() {
|
|
const input = document.getElementById('taskInput');
|
|
const btn = document.getElementById('submitBtn');
|
|
const text = input.value.trim();
|
|
if (!text) return;
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = 'Launching...';
|
|
|
|
try {
|
|
const contextId = crypto.randomUUID();
|
|
const res = await fetch(`${BASE}/tasks/${contextId}/send`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
jsonrpc: '2.0',
|
|
id: crypto.randomUUID(),
|
|
method: 'message/send',
|
|
params: {
|
|
message: {
|
|
role: 'user',
|
|
parts: [{ type: 'text', text }]
|
|
}
|
|
}
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
input.value = '';
|
|
refresh();
|
|
} catch (err) {
|
|
alert('Error: ' + err.message);
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = 'Launch Swarm';
|
|
}
|
|
}
|
|
|
|
async function refresh() {
|
|
try {
|
|
// Health
|
|
const health = await fetchJSON('/health');
|
|
document.getElementById('stat-total').textContent = health.totalSwarms;
|
|
document.getElementById('stat-running').textContent = health.activeSwarms;
|
|
document.getElementById('stat-workers').textContent = health.workerAgents;
|
|
document.getElementById('stat-events').textContent = health.totalEvents;
|
|
|
|
// Swarms
|
|
const swarmsData = await fetchJSON('/swarms');
|
|
document.getElementById('stat-completed').textContent = swarmsData.swarms.filter(s => s.state === 'completed').length;
|
|
|
|
const swarmList = document.getElementById('swarmList');
|
|
if (swarmsData.swarms.length === 0) {
|
|
swarmList.innerHTML = '<p style="color: #565f89;">No swarms yet. Submit a task above.</p>';
|
|
} else {
|
|
swarmList.innerHTML = swarmsData.swarms.reverse().map(s => `
|
|
<div class="swarm-card">
|
|
<div class="swarm-header">
|
|
<span class="badge ${s.state}">${s.state}</span>
|
|
<span style="font-size:11px;color:#565f89">${new Date(s.createdAt).toLocaleTimeString()}</span>
|
|
</div>
|
|
<div class="instruction">${escHtml(s.instruction)}</div>
|
|
<div style="font-size:12px;color:#565f89">${s.subtaskCount} subtask(s)</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Workers
|
|
const agents = await fetchJSON('/agents');
|
|
const workerList = document.getElementById('workerList');
|
|
workerList.innerHTML = agents.workers.map(w => `
|
|
<div class="worker-card">
|
|
<div class="worker-icon ${w.id}">${w.id[0].toUpperCase()}</div>
|
|
<div class="worker-info">
|
|
<div class="name">${escHtml(w.name)}</div>
|
|
<div class="skills">${w.skills.join(', ')}</div>
|
|
</div>
|
|
<div class="worker-cost">${w.costPerTask}</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Events
|
|
const eventsData = await fetchJSON('/events?last=20');
|
|
const eventLog = document.getElementById('eventLog');
|
|
if (eventsData.events.length === 0) {
|
|
eventLog.innerHTML = '<p style="color: #565f89;">No events yet.</p>';
|
|
} else {
|
|
eventLog.innerHTML = eventsData.events.reverse().map(e => `
|
|
<div class="event-row">
|
|
<span class="event-time">${new Date(e.timestamp).toLocaleTimeString()}</span>
|
|
<span class="event-action">${escHtml(e.action)}</span>
|
|
<span class="event-data">${escHtml(JSON.stringify(Object.fromEntries(Object.entries(e).filter(([k]) => !['timestamp','agentId','action'].includes(k)))))}</span>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
} catch (err) {
|
|
console.error('Refresh error:', err);
|
|
}
|
|
}
|
|
|
|
function escHtml(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s || '';
|
|
return d.innerHTML;
|
|
}
|
|
|
|
refresh();
|
|
setInterval(refresh, 3000);
|
|
</script>
|
|
</body>
|
|
</html>
|