tao-shen Claude Opus 4.6 commited on
Commit
e6586a2
Β·
1 Parent(s): 8ee58f9

feat: add remote agent monitoring and A2A activity tracking to proxy

Browse files

- Poll remote agents (Adam/Eve) via REMOTE_AGENTS env var
- Intercept /agents endpoint to merge OpenClaw + remote agent states
- Track A2A POST requests to show "writing" state during communication
- Remote agent states mapped to Office areas (breakroom/writing/error)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. scripts/a2a-proxy.cjs +145 -0
scripts/a2a-proxy.cjs CHANGED
@@ -5,6 +5,7 @@
5
  * /.well-known/* β†’ A2A gateway (port 18800)
6
  * /a2a/* β†’ A2A gateway (port 18800)
7
  * /api/state β†’ local state JSON (for Office frontend polling)
 
8
  * everything else β†’ OpenClaw (port 7861)
9
  */
10
  'use strict';
@@ -17,6 +18,16 @@ const OPENCLAW_PORT = 7861;
17
  const A2A_PORT = 18800;
18
  const AGENT_NAME = process.env.AGENT_NAME || 'Agent';
19
 
 
 
 
 
 
 
 
 
 
 
20
  let currentState = {
21
  state: 'syncing',
22
  detail: `${AGENT_NAME} is starting...`,
@@ -24,8 +35,90 @@ let currentState = {
24
  updated_at: new Date().toISOString()
25
  };
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  // Poll OpenClaw health to track state
28
  async function pollOpenClawHealth() {
 
 
29
  try {
30
  const controller = new AbortController();
31
  const timeout = setTimeout(() => controller.abort(), 5000);
@@ -52,6 +145,34 @@ async function pollOpenClawHealth() {
52
  setInterval(pollOpenClawHealth, 5000);
53
  pollOpenClawHealth();
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  function proxyRequest(req, res, targetPort) {
56
  const options = {
57
  hostname: '127.0.0.1',
@@ -81,6 +202,11 @@ const server = http.createServer((req, res) => {
81
 
82
  // A2A routes β†’ A2A gateway
83
  if (pathname.startsWith('/.well-known/') || pathname.startsWith('/a2a/')) {
 
 
 
 
 
84
  return proxyRequest(req, res, A2A_PORT);
85
  }
86
 
@@ -93,6 +219,25 @@ const server = http.createServer((req, res) => {
93
  return res.end(JSON.stringify(currentState));
94
  }
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  // Everything else β†’ OpenClaw
97
  proxyRequest(req, res, OPENCLAW_PORT);
98
  });
 
5
  * /.well-known/* β†’ A2A gateway (port 18800)
6
  * /a2a/* β†’ A2A gateway (port 18800)
7
  * /api/state β†’ local state JSON (for Office frontend polling)
8
+ * /agents β†’ merged agent list (OpenClaw + remote agents)
9
  * everything else β†’ OpenClaw (port 7861)
10
  */
11
  'use strict';
 
18
  const A2A_PORT = 18800;
19
  const AGENT_NAME = process.env.AGENT_NAME || 'Agent';
20
 
21
+ // Remote agents to monitor (comma-separated URLs)
22
+ // e.g. REMOTE_AGENTS=adam|Adam|https://tao-shen-huggingclaw-adam.hf.space,eve|Eve|https://tao-shen-huggingclaw-eve.hf.space
23
+ const REMOTE_AGENTS_RAW = process.env.REMOTE_AGENTS || '';
24
+ const remoteAgents = REMOTE_AGENTS_RAW
25
+ ? REMOTE_AGENTS_RAW.split(',').map(entry => {
26
+ const [id, name, baseUrl] = entry.trim().split('|');
27
+ return { id, name, baseUrl };
28
+ }).filter(a => a.id && a.name && a.baseUrl)
29
+ : [];
30
+
31
  let currentState = {
32
  state: 'syncing',
33
  detail: `${AGENT_NAME} is starting...`,
 
35
  updated_at: new Date().toISOString()
36
  };
37
 
38
+ // Track A2A activity β€” when an A2A message is being processed,
39
+ // temporarily switch state to 'writing' so frontends can see it
40
+ let a2aActiveRequests = 0;
41
+ let a2aIdleTimer = null;
42
+ const A2A_IDLE_DELAY = 8000; // stay "writing" for 8s after last A2A request ends
43
+
44
+ function markA2AActive() {
45
+ a2aActiveRequests++;
46
+ if (a2aIdleTimer) { clearTimeout(a2aIdleTimer); a2aIdleTimer = null; }
47
+ currentState = {
48
+ state: 'writing',
49
+ detail: `${AGENT_NAME} is communicating...`,
50
+ progress: 100,
51
+ updated_at: new Date().toISOString()
52
+ };
53
+ }
54
+
55
+ function markA2ADone() {
56
+ a2aActiveRequests = Math.max(0, a2aActiveRequests - 1);
57
+ if (a2aActiveRequests === 0) {
58
+ if (a2aIdleTimer) clearTimeout(a2aIdleTimer);
59
+ a2aIdleTimer = setTimeout(() => {
60
+ a2aIdleTimer = null;
61
+ pollOpenClawHealth();
62
+ }, A2A_IDLE_DELAY);
63
+ }
64
+ }
65
+
66
+ // Remote agent states (polled periodically)
67
+ const remoteAgentStates = new Map();
68
+
69
+ async function pollRemoteAgent(agent) {
70
+ try {
71
+ const controller = new AbortController();
72
+ const timeout = setTimeout(() => controller.abort(), 5000);
73
+ const resp = await fetch(`${agent.baseUrl}/api/state`, {
74
+ signal: controller.signal
75
+ });
76
+ clearTimeout(timeout);
77
+ if (resp.ok) {
78
+ const data = await resp.json();
79
+ remoteAgentStates.set(agent.id, {
80
+ agentId: agent.id,
81
+ name: agent.name,
82
+ state: data.state || 'idle',
83
+ detail: data.detail || '',
84
+ area: (data.state === 'idle') ? 'breakroom'
85
+ : (data.state === 'error') ? 'error'
86
+ : 'writing',
87
+ authStatus: 'approved',
88
+ updated_at: data.updated_at
89
+ });
90
+ }
91
+ } catch (_) {
92
+ // Keep last known state or mark as offline
93
+ if (!remoteAgentStates.has(agent.id)) {
94
+ remoteAgentStates.set(agent.id, {
95
+ agentId: agent.id,
96
+ name: agent.name,
97
+ state: 'syncing',
98
+ detail: `${agent.name} is starting...`,
99
+ area: 'door',
100
+ authStatus: 'approved'
101
+ });
102
+ }
103
+ }
104
+ }
105
+
106
+ function pollAllRemoteAgents() {
107
+ for (const agent of remoteAgents) {
108
+ pollRemoteAgent(agent);
109
+ }
110
+ }
111
+
112
+ if (remoteAgents.length > 0) {
113
+ setInterval(pollAllRemoteAgents, 5000);
114
+ pollAllRemoteAgents();
115
+ console.log(`[a2a-proxy] Monitoring ${remoteAgents.length} remote agent(s): ${remoteAgents.map(a => a.name).join(', ')}`);
116
+ }
117
+
118
  // Poll OpenClaw health to track state
119
  async function pollOpenClawHealth() {
120
+ // Don't overwrite active A2A state
121
+ if (a2aActiveRequests > 0 || a2aIdleTimer) return;
122
  try {
123
  const controller = new AbortController();
124
  const timeout = setTimeout(() => controller.abort(), 5000);
 
145
  setInterval(pollOpenClawHealth, 5000);
146
  pollOpenClawHealth();
147
 
148
+ // Fetch agents from OpenClaw and merge with remote agents
149
+ async function getMergedAgents() {
150
+ let openClawAgents = [];
151
+ try {
152
+ const controller = new AbortController();
153
+ const timeout = setTimeout(() => controller.abort(), 3000);
154
+ const resp = await fetch(`http://127.0.0.1:${OPENCLAW_PORT}/agents`, {
155
+ signal: controller.signal
156
+ });
157
+ clearTimeout(timeout);
158
+ if (resp.ok) {
159
+ openClawAgents = await resp.json();
160
+ if (!Array.isArray(openClawAgents)) openClawAgents = [];
161
+ }
162
+ } catch (_) {}
163
+
164
+ // Merge: OpenClaw agents + remote agents (deduplicate by agentId)
165
+ const existingIds = new Set(openClawAgents.map(a => a.agentId));
166
+ const merged = [...openClawAgents];
167
+ let slotIndex = openClawAgents.length;
168
+ for (const [id, agentState] of remoteAgentStates) {
169
+ if (!existingIds.has(id)) {
170
+ merged.push({ ...agentState, _slotIndex: slotIndex++ });
171
+ }
172
+ }
173
+ return merged;
174
+ }
175
+
176
  function proxyRequest(req, res, targetPort) {
177
  const options = {
178
  hostname: '127.0.0.1',
 
202
 
203
  // A2A routes β†’ A2A gateway
204
  if (pathname.startsWith('/.well-known/') || pathname.startsWith('/a2a/')) {
205
+ // Track POST requests (message/send) as active communication
206
+ if (req.method === 'POST') {
207
+ markA2AActive();
208
+ res.on('finish', markA2ADone);
209
+ }
210
  return proxyRequest(req, res, A2A_PORT);
211
  }
212
 
 
219
  return res.end(JSON.stringify(currentState));
220
  }
221
 
222
+ // Agents endpoint β€” merge OpenClaw agents with remote agents
223
+ if (pathname === '/agents' && req.method === 'GET') {
224
+ getMergedAgents().then(agents => {
225
+ res.writeHead(200, {
226
+ 'Content-Type': 'application/json',
227
+ 'Access-Control-Allow-Origin': '*'
228
+ });
229
+ res.end(JSON.stringify(agents));
230
+ }).catch(() => {
231
+ // Fallback: just return remote agents
232
+ res.writeHead(200, {
233
+ 'Content-Type': 'application/json',
234
+ 'Access-Control-Allow-Origin': '*'
235
+ });
236
+ res.end(JSON.stringify([...remoteAgentStates.values()]));
237
+ });
238
+ return;
239
+ }
240
+
241
  // Everything else β†’ OpenClaw
242
  proxyRequest(req, res, OPENCLAW_PORT);
243
  });