eacn3 0.3.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +345 -0
- package/dist/index.js +259 -48
- package/dist/index.js.map +1 -1
- package/dist/server.js +978 -75
- package/dist/server.js.map +1 -1
- package/dist/src/a2a-server.js +2 -1
- package/dist/src/a2a-server.js.map +1 -1
- package/dist/src/event-transport.d.ts +48 -0
- package/dist/src/event-transport.js +156 -0
- package/dist/src/event-transport.js.map +1 -0
- package/dist/src/models.d.ts +59 -11
- package/dist/src/models.js +1 -1
- package/dist/src/models.js.map +1 -1
- package/dist/src/network-client.d.ts +3 -1
- package/dist/src/network-client.js +87 -14
- package/dist/src/network-client.js.map +1 -1
- package/dist/src/reverse-control.d.ts +74 -0
- package/dist/src/reverse-control.js +609 -0
- package/dist/src/reverse-control.js.map +1 -0
- package/dist/src/state.d.ts +50 -4
- package/dist/src/state.js +492 -43
- package/dist/src/state.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -7
- package/scripts/cli.cjs +28 -11
- package/skills/eacn3-bid/SKILL.md +2 -2
- package/skills/eacn3-bid-zh/SKILL.md +2 -2
- package/skills/eacn3-bounty/SKILL.md +7 -7
- package/skills/eacn3-bounty-zh/SKILL.md +7 -7
- package/skills/eacn3-budget/SKILL.md +1 -1
- package/skills/eacn3-budget-zh/SKILL.md +1 -1
- package/skills/eacn3-clarify/SKILL.md +1 -1
- package/skills/eacn3-clarify-zh/SKILL.md +1 -1
- package/skills/eacn3-collect/SKILL.md +1 -1
- package/skills/eacn3-collect-zh/SKILL.md +1 -1
- package/skills/eacn3-dashboard/SKILL.md +3 -3
- package/skills/eacn3-dashboard-zh/SKILL.md +3 -3
- package/skills/eacn3-delegate/SKILL.md +1 -1
- package/skills/eacn3-delegate-zh/SKILL.md +1 -1
- package/skills/eacn3-execute/SKILL.md +1 -1
- package/skills/eacn3-execute-zh/SKILL.md +1 -1
- package/skills/eacn3-invite/SKILL.md +1 -1
- package/skills/eacn3-invite-zh/SKILL.md +1 -1
- package/skills/eacn3-task/SKILL.md +1 -1
- package/skills/eacn3-task-zh/SKILL.md +1 -1
package/dist/server.js
CHANGED
|
@@ -11,26 +11,59 @@ import { z } from "zod";
|
|
|
11
11
|
import { EACN3_DEFAULT_NETWORK_ENDPOINT } from "./src/models.js";
|
|
12
12
|
import * as state from "./src/state.js";
|
|
13
13
|
import * as net from "./src/network-client.js";
|
|
14
|
-
import * as
|
|
14
|
+
import * as transport from "./src/event-transport.js";
|
|
15
15
|
import * as a2a from "./src/a2a-server.js";
|
|
16
|
+
import * as rc from "./src/reverse-control.js";
|
|
17
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
18
|
+
import { join, dirname } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Activity log — file-based traceability
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
const __log_dir = join(dirname(fileURLToPath(import.meta.url)), "..", "logs");
|
|
24
|
+
let __activity_log = null;
|
|
25
|
+
function _writeActivityLog(line) {
|
|
26
|
+
try {
|
|
27
|
+
if (!__activity_log) {
|
|
28
|
+
mkdirSync(__log_dir, { recursive: true });
|
|
29
|
+
__activity_log = join(__log_dir, "activity.log");
|
|
30
|
+
}
|
|
31
|
+
appendFileSync(__activity_log, line + "\n");
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
}
|
|
16
35
|
// ---------------------------------------------------------------------------
|
|
17
36
|
// Helper: MCP text result
|
|
18
37
|
// ---------------------------------------------------------------------------
|
|
19
38
|
function ok(data) {
|
|
20
|
-
|
|
39
|
+
const result = {
|
|
40
|
+
content: [{ type: "text", text: JSON.stringify(data) }],
|
|
41
|
+
};
|
|
42
|
+
// Fallback directive injection: when sampling is unavailable,
|
|
43
|
+
// append pending event directives to any tool response so the
|
|
44
|
+
// Host LLM sees actionable events without explicit polling.
|
|
45
|
+
const directives = rc.drainDirectives();
|
|
46
|
+
if (directives) {
|
|
47
|
+
result.content.push({ type: "text", text: directives });
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
21
50
|
}
|
|
22
51
|
function err(message) {
|
|
23
52
|
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }] };
|
|
24
53
|
}
|
|
25
|
-
/** Log MCP tool calls to stderr for traceability. */
|
|
54
|
+
/** Log MCP tool calls to stderr AND activity.log for traceability. */
|
|
26
55
|
function logToolCall(toolName, params) {
|
|
27
56
|
const ts = new Date().toISOString();
|
|
28
|
-
|
|
57
|
+
const line = `[MCP] ${ts} CALL ${toolName} params=${JSON.stringify(params)}`;
|
|
58
|
+
console.error(line);
|
|
59
|
+
_writeActivityLog(line);
|
|
29
60
|
}
|
|
30
61
|
function logToolResult(toolName, success, detail) {
|
|
31
62
|
const ts = new Date().toISOString();
|
|
32
63
|
const tag = success ? "OK" : "ERR";
|
|
33
|
-
|
|
64
|
+
const line = `[MCP] ${ts} ${tag} ${toolName}${detail ? ` ${detail}` : ""}`;
|
|
65
|
+
console.error(line);
|
|
66
|
+
_writeActivityLog(line);
|
|
34
67
|
}
|
|
35
68
|
/**
|
|
36
69
|
* Resolve agent ID: use provided value, or auto-inject from state.
|
|
@@ -47,18 +80,117 @@ function resolveAgentId(provided) {
|
|
|
47
80
|
throw new Error("No agents registered. Call eacn3_register_agent first.");
|
|
48
81
|
throw new Error(`Multiple agents registered (${agents.map(a => a.agent_id).join(", ")}). Specify agent_id explicitly.`);
|
|
49
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Detect whether an API error indicates the server/agent registration is stale
|
|
85
|
+
* on the backend (e.g. "server not found", "agent not found", 404 on discovery).
|
|
86
|
+
*/
|
|
87
|
+
function isStaleRegistrationError(e) {
|
|
88
|
+
const msg = (e instanceof Error ? e.message : String(e)).toLowerCase();
|
|
89
|
+
return (msg.includes("404") ||
|
|
90
|
+
msg.includes("not found") ||
|
|
91
|
+
msg.includes("not registered") ||
|
|
92
|
+
msg.includes("unknown server") ||
|
|
93
|
+
msg.includes("unknown agent"));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Wrap a tool's async handler to auto-reconnect on stale registration errors.
|
|
97
|
+
* If the first attempt fails because the backend forgot our server/agent,
|
|
98
|
+
* trigger autoReconnect() and retry once.
|
|
99
|
+
*/
|
|
100
|
+
async function withAutoReconnect(fn) {
|
|
101
|
+
try {
|
|
102
|
+
return await fn();
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
if (isStaleRegistrationError(e) && !reconnecting) {
|
|
106
|
+
console.error(`[EACN3] stale registration detected, auto-reconnecting before retry...`);
|
|
107
|
+
await autoReconnect();
|
|
108
|
+
// Retry once after reconnect
|
|
109
|
+
return await fn();
|
|
110
|
+
}
|
|
111
|
+
throw e;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
50
114
|
// ---------------------------------------------------------------------------
|
|
51
|
-
// Heartbeat background interval
|
|
115
|
+
// Heartbeat background interval with auto-reconnect
|
|
52
116
|
// ---------------------------------------------------------------------------
|
|
53
117
|
let heartbeatInterval = null;
|
|
118
|
+
let heartbeatConsecutiveFailures = 0;
|
|
119
|
+
let reconnecting = false;
|
|
120
|
+
const HEARTBEAT_FAIL_THRESHOLD = 2; // trigger reconnect after 2 consecutive heartbeat failures
|
|
121
|
+
/**
|
|
122
|
+
* Attempt to re-register server and agents with the network.
|
|
123
|
+
* Called when heartbeat failures indicate the backend has forgotten us.
|
|
124
|
+
*/
|
|
125
|
+
async function autoReconnect() {
|
|
126
|
+
if (reconnecting)
|
|
127
|
+
return;
|
|
128
|
+
reconnecting = true;
|
|
129
|
+
console.error("[EACN3] auto-reconnect: heartbeat failures exceeded threshold, re-registering...");
|
|
130
|
+
const s = state.getState();
|
|
131
|
+
try {
|
|
132
|
+
// Try heartbeat once more — maybe transient
|
|
133
|
+
try {
|
|
134
|
+
await net.heartbeat();
|
|
135
|
+
heartbeatConsecutiveFailures = 0;
|
|
136
|
+
console.error("[EACN3] auto-reconnect: heartbeat recovered, no re-registration needed");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
catch { /* still failing */ }
|
|
140
|
+
// Re-register server
|
|
141
|
+
const res = await net.registerServer("0.5.1", "plugin://local", "plugin-user");
|
|
142
|
+
const newSid = res.server_id;
|
|
143
|
+
s.server_card = {
|
|
144
|
+
server_id: newSid,
|
|
145
|
+
version: "0.5.1",
|
|
146
|
+
endpoint: "plugin://local",
|
|
147
|
+
owner: "plugin-user",
|
|
148
|
+
status: "online",
|
|
149
|
+
};
|
|
150
|
+
state.saveServerData();
|
|
151
|
+
// Re-register all owned agents with the new server_id
|
|
152
|
+
for (const agent of state.listAgents()) {
|
|
153
|
+
agent.server_id = newSid;
|
|
154
|
+
try {
|
|
155
|
+
await net.registerAgent(agent);
|
|
156
|
+
console.error(`[EACN3] auto-reconnect: re-registered agent ${agent.agent_id}`);
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
console.error(`[EACN3] auto-reconnect: failed to re-register agent ${agent.agent_id}: ${e.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
state.save();
|
|
163
|
+
heartbeatConsecutiveFailures = 0;
|
|
164
|
+
console.error("[EACN3] auto-reconnect: completed successfully");
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
console.error(`[EACN3] auto-reconnect: failed: ${e.message}`);
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
reconnecting = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
54
173
|
function startHeartbeat() {
|
|
55
174
|
if (heartbeatInterval)
|
|
56
175
|
return;
|
|
176
|
+
heartbeatConsecutiveFailures = 0;
|
|
177
|
+
// Wire up the connection-degraded callback from network-client
|
|
178
|
+
net.setConnectionDegradedCallback(() => {
|
|
179
|
+
console.error("[EACN3] connection degraded (multiple request failures), scheduling reconnect");
|
|
180
|
+
autoReconnect();
|
|
181
|
+
});
|
|
57
182
|
heartbeatInterval = setInterval(async () => {
|
|
58
183
|
try {
|
|
59
184
|
await net.heartbeat();
|
|
185
|
+
heartbeatConsecutiveFailures = 0;
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
heartbeatConsecutiveFailures++;
|
|
189
|
+
console.error(`[EACN3] heartbeat failed (${heartbeatConsecutiveFailures}/${HEARTBEAT_FAIL_THRESHOLD}): ${e.message}`);
|
|
190
|
+
if (heartbeatConsecutiveFailures >= HEARTBEAT_FAIL_THRESHOLD) {
|
|
191
|
+
autoReconnect();
|
|
192
|
+
}
|
|
60
193
|
}
|
|
61
|
-
catch { /* silent */ }
|
|
62
194
|
}, 60_000);
|
|
63
195
|
}
|
|
64
196
|
function stopHeartbeat() {
|
|
@@ -66,11 +198,14 @@ function stopHeartbeat() {
|
|
|
66
198
|
clearInterval(heartbeatInterval);
|
|
67
199
|
heartbeatInterval = null;
|
|
68
200
|
}
|
|
201
|
+
heartbeatConsecutiveFailures = 0;
|
|
69
202
|
}
|
|
70
203
|
// ---------------------------------------------------------------------------
|
|
71
204
|
// MCP Server
|
|
72
205
|
// ---------------------------------------------------------------------------
|
|
73
|
-
const server = new McpServer({ name: "eacn3", version: "0.
|
|
206
|
+
const server = new McpServer({ name: "eacn3", version: "0.5.1" }, {
|
|
207
|
+
capabilities: { logging: {} },
|
|
208
|
+
});
|
|
74
209
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
75
210
|
// Health / Cluster (2)
|
|
76
211
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -78,12 +213,15 @@ const server = new McpServer({ name: "eacn3", version: "0.3.0" });
|
|
|
78
213
|
server.tool("eacn3_health", "Check if a network node is alive and responding. No prerequisites — works before eacn3_connect. Returns {status: 'ok'} on success. Use this to verify an endpoint before connecting.", {
|
|
79
214
|
endpoint: z.string().optional().describe("Node URL to probe. Defaults to configured network endpoint."),
|
|
80
215
|
}, async (params) => {
|
|
216
|
+
logToolCall("eacn3_health", params);
|
|
81
217
|
const target = params.endpoint ?? state.getState().network_endpoint;
|
|
82
218
|
try {
|
|
83
219
|
const health = await net.checkHealth(target);
|
|
220
|
+
logToolResult("eacn3_health", true);
|
|
84
221
|
return ok({ endpoint: target, ...health });
|
|
85
222
|
}
|
|
86
223
|
catch (e) {
|
|
224
|
+
logToolResult("eacn3_health", false, e.message);
|
|
87
225
|
return err(`Health check failed for ${target}: ${e.message}`);
|
|
88
226
|
}
|
|
89
227
|
});
|
|
@@ -104,7 +242,7 @@ server.tool("eacn3_cluster_status", "Retrieve the full cluster topology includin
|
|
|
104
242
|
// Server Management (4)
|
|
105
243
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
106
244
|
// #1 eacn3_connect
|
|
107
|
-
server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your FIRST call. Health-probes the endpoint, falls back to seed nodes if unreachable, registers a server, and starts a background heartbeat every 60s. Returns {server_id, network_endpoint, fallback,
|
|
245
|
+
server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your FIRST call. Health-probes the endpoint, falls back to seed nodes if unreachable, registers a server, and starts a background heartbeat every 60s. Returns {server_id, network_endpoint, fallback, available_agents, hint}. IMPORTANT: agents are NOT auto-restored. Check available_agents — if you have a previous agent, call eacn3_claim_agent(agent_id) to resume it. Otherwise call eacn3_register_agent() to create a new one. Only one agent per session.", {
|
|
108
246
|
network_endpoint: z.string().optional().describe(`Network URL. Defaults to ${EACN3_DEFAULT_NETWORK_ENDPOINT}`),
|
|
109
247
|
seed_nodes: z.array(z.string()).optional().describe("Additional seed node URLs for fallback"),
|
|
110
248
|
}, async (params) => {
|
|
@@ -132,11 +270,11 @@ server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your
|
|
|
132
270
|
}
|
|
133
271
|
catch {
|
|
134
272
|
// Server no longer known to network — re-register
|
|
135
|
-
const res = await net.registerServer("0.
|
|
273
|
+
const res = await net.registerServer("0.5.1", "plugin://local", "plugin-user");
|
|
136
274
|
sid = res.server_id;
|
|
137
275
|
s.server_card = {
|
|
138
276
|
server_id: sid,
|
|
139
|
-
version: "0.
|
|
277
|
+
version: "0.5.1",
|
|
140
278
|
endpoint: "plugin://local",
|
|
141
279
|
owner: "plugin-user",
|
|
142
280
|
status: "online",
|
|
@@ -152,62 +290,61 @@ server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your
|
|
|
152
290
|
}
|
|
153
291
|
}
|
|
154
292
|
else {
|
|
155
|
-
const res = await net.registerServer("0.
|
|
293
|
+
const res = await net.registerServer("0.5.1", "plugin://local", "plugin-user");
|
|
156
294
|
sid = res.server_id;
|
|
157
295
|
s.server_card = {
|
|
158
296
|
server_id: sid,
|
|
159
|
-
version: "0.
|
|
297
|
+
version: "0.5.1",
|
|
160
298
|
endpoint: "plugin://local",
|
|
161
299
|
owner: "plugin-user",
|
|
162
300
|
status: "online",
|
|
163
301
|
};
|
|
164
302
|
}
|
|
165
|
-
state.
|
|
303
|
+
state.saveServerData();
|
|
166
304
|
// Start background heartbeat
|
|
167
305
|
startHeartbeat();
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
await net.getAgentInfo(agent.agent_id);
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
// Agent not found on network (e.g. server restarted with in-memory DB)
|
|
175
|
-
try {
|
|
176
|
-
await net.registerAgent(agent);
|
|
177
|
-
}
|
|
178
|
-
catch { /* best-effort */ }
|
|
179
|
-
}
|
|
180
|
-
ws.connect(agent.agent_id);
|
|
181
|
-
}
|
|
182
|
-
const restoredAgents = Object.values(s.agents).map((a) => ({
|
|
183
|
-
agent_id: a.agent_id,
|
|
184
|
-
name: a.name,
|
|
185
|
-
domains: a.domains,
|
|
186
|
-
tier: a.tier,
|
|
187
|
-
}));
|
|
306
|
+
// List agents available on disk (from previous sessions) — do NOT auto-restore.
|
|
307
|
+
// Each session must explicitly claim an agent via eacn3_claim_agent.
|
|
308
|
+
const availableAgents = state.listAvailableAgents();
|
|
188
309
|
return ok({
|
|
189
310
|
connected: true,
|
|
190
311
|
server_id: sid,
|
|
191
312
|
network_endpoint: endpoint,
|
|
192
313
|
fallback,
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
? "You have previously registered agents restored and reconnected. You can use them directly without re-registering. Call eacn3_list_my_agents() for full details."
|
|
314
|
+
available_agents: availableAgents,
|
|
315
|
+
hint: availableAgents.length > 0
|
|
316
|
+
? "Previous agents found on disk. Call eacn3_claim_agent(agent_id) to resume one, or eacn3_register_agent() to create a new one."
|
|
197
317
|
: "No previous agents found. Register a new agent with eacn3_register_agent().",
|
|
318
|
+
toolkit: {
|
|
319
|
+
workflow: {
|
|
320
|
+
eacn3_next: "Periodic dispatch loop — call on a regular interval to process pending events one at a time. Returns the highest-priority event with action directives. This is the ONLY tool that spans the full task lifecycle.",
|
|
321
|
+
eacn3_get_events: "Drain all pending events at once (bulk alternative to next).",
|
|
322
|
+
},
|
|
323
|
+
team: {
|
|
324
|
+
eacn3_team_setup: "Form a team of agents around a shared git repo via ACK message exchange.",
|
|
325
|
+
eacn3_team_status: "Check team formation progress — which ACKs are exchanged, which peer branches are known.",
|
|
326
|
+
},
|
|
327
|
+
task: {
|
|
328
|
+
eacn3_create_task: "Publish a task (supports team_id to auto-inject team preamble).",
|
|
329
|
+
eacn3_create_subtask: "Delegate part of your work as a child task.",
|
|
330
|
+
eacn3_submit_bid: "Bid on a task you want to execute.",
|
|
331
|
+
eacn3_submit_result: "Submit your completed work.",
|
|
332
|
+
eacn3_select_result: "Pick the winning result (triggers payment).",
|
|
333
|
+
},
|
|
334
|
+
},
|
|
198
335
|
});
|
|
199
336
|
});
|
|
200
337
|
// #2 eacn3_disconnect
|
|
201
|
-
server.tool("eacn3_disconnect", "Disconnect from the EACN3 network
|
|
338
|
+
server.tool("eacn3_disconnect", "Disconnect from the EACN3 network. Requires: eacn3_connect first. Side effects: active tasks will timeout and hurt reputation. Server identity is preserved — on next eacn3_connect you can claim your agent back via eacn3_claim_agent. Returns {disconnected: true}. Only call at end of session.", {}, async () => {
|
|
202
339
|
stopHeartbeat();
|
|
203
|
-
|
|
340
|
+
transport.disconnectAll();
|
|
204
341
|
// Do NOT call unregisterServer — it cascade-deletes all agents on the network side.
|
|
205
342
|
// We only go offline; identity is preserved for reconnection.
|
|
206
343
|
const s = state.getState();
|
|
207
|
-
|
|
344
|
+
// Don't write server.json — other sessions may still be using this server.
|
|
345
|
+
// Just clean up this session's in-memory state.
|
|
346
|
+
if (s.server_card)
|
|
208
347
|
s.server_card.status = "offline";
|
|
209
|
-
}
|
|
210
|
-
state.save();
|
|
211
348
|
return ok({ disconnected: true });
|
|
212
349
|
});
|
|
213
350
|
// #3 eacn3_heartbeat
|
|
@@ -237,11 +374,48 @@ server.tool("eacn3_server_info", "Get current server connection state, including
|
|
|
237
374
|
});
|
|
238
375
|
});
|
|
239
376
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
240
|
-
// Agent Management (
|
|
377
|
+
// Agent Management (8)
|
|
241
378
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
379
|
+
// #4b eacn3_claim_agent
|
|
380
|
+
server.tool("eacn3_claim_agent", "Claim a previously registered agent from disk into this session. Use this to resume an agent listed in available_agents from eacn3_connect. The agent is re-registered on the network and event transport is started. Only one agent per session.", {
|
|
381
|
+
agent_id: z.string().describe("ID of the agent to claim (from available_agents)"),
|
|
382
|
+
}, async (params) => {
|
|
383
|
+
logToolCall("eacn3_claim_agent", params);
|
|
384
|
+
if (state.listAgents().length > 0) {
|
|
385
|
+
return err("This session already has an agent. Only one agent per session.");
|
|
386
|
+
}
|
|
387
|
+
const agent = state.claimAgent(params.agent_id);
|
|
388
|
+
if (!agent) {
|
|
389
|
+
return err(`Agent ${params.agent_id} not found on disk. Use eacn3_register_agent to create a new one.`);
|
|
390
|
+
}
|
|
391
|
+
// Re-register on network with current session's server_id
|
|
392
|
+
const s = state.getState();
|
|
393
|
+
const oldServerId = agent.server_id;
|
|
394
|
+
if (s.server_card)
|
|
395
|
+
agent.server_id = s.server_card.server_id;
|
|
396
|
+
try {
|
|
397
|
+
await net.registerAgent(agent);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
agent.server_id = oldServerId; // Revert on failure to stay consistent with network
|
|
401
|
+
}
|
|
402
|
+
// Start event transport
|
|
403
|
+
transport.connect(agent.agent_id);
|
|
404
|
+
// Initialize reverse control
|
|
405
|
+
rc.configure(agent.agent_id);
|
|
406
|
+
state.save();
|
|
407
|
+
logToolResult("eacn3_claim_agent", true, agent.agent_id);
|
|
408
|
+
return ok({
|
|
409
|
+
claimed: true,
|
|
410
|
+
agent_id: agent.agent_id,
|
|
411
|
+
name: agent.name,
|
|
412
|
+
domains: agent.domains,
|
|
413
|
+
tier: agent.tier,
|
|
414
|
+
});
|
|
415
|
+
});
|
|
242
416
|
// #5 eacn3_register_agent
|
|
243
417
|
// Inlines: adapter (AgentCard assembly) + registry (validate + persist + DHT)
|
|
244
|
-
server.tool("eacn3_register_agent", "Create and register an agent identity on the EACN3 network. Requires: eacn3_connect first. Assembles an AgentCard, registers it with the network, persists it locally, and
|
|
418
|
+
server.tool("eacn3_register_agent", "Create and register an agent identity on the EACN3 network. Requires: eacn3_connect first. Assembles an AgentCard, registers it with the network, persists it locally, and starts polling for push events (task_broadcast, subtask_completed, etc.). Returns {agent_id, seeds, domains}. Domains control which task broadcasts you receive — be specific (e.g. 'python-coding' not 'coding').", {
|
|
245
419
|
name: z.string().describe("Agent display name"),
|
|
246
420
|
description: z.string().describe("What this Agent does"),
|
|
247
421
|
domains: z.array(z.string()).describe("Capability domains (e.g. ['translation', 'coding'])"),
|
|
@@ -260,6 +434,11 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
|
|
|
260
434
|
agent_id: z.string().optional().describe("Custom agent ID. Auto-generated if omitted."),
|
|
261
435
|
a2a_port: z.number().optional().describe("Port for A2A HTTP server. Enables direct agent-to-agent messaging. Omit to use Network relay only."),
|
|
262
436
|
a2a_url: z.string().optional().describe("Full public URL for A2A callbacks (e.g. 'http://my-server.com:3001'). Auto-generated from a2a_port if omitted."),
|
|
437
|
+
reverse_control: z.object({
|
|
438
|
+
enabled: z.boolean().optional().describe("Enable MCP reverse control (sampling/notifications). Default true."),
|
|
439
|
+
sampling_events: z.array(z.string()).optional().describe("Event types that trigger LLM sampling (e.g. ['task_broadcast', 'direct_message']). Default: all actionable events."),
|
|
440
|
+
notification_events: z.array(z.string()).optional().describe("Event types that send notifications only (e.g. ['task_collected', 'task_timeout']). Default: status events."),
|
|
441
|
+
}).optional().describe("Configure MCP reverse control — lets the network proactively drive your agent via sampling requests."),
|
|
263
442
|
}, async (params) => {
|
|
264
443
|
const s = state.getState();
|
|
265
444
|
if (!s.server_card)
|
|
@@ -300,8 +479,21 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
|
|
|
300
479
|
const res = await net.registerAgent(card);
|
|
301
480
|
// Persist locally
|
|
302
481
|
state.addAgent(card);
|
|
303
|
-
//
|
|
304
|
-
|
|
482
|
+
// Register agent for on-demand event fetching
|
|
483
|
+
transport.connect(agentId);
|
|
484
|
+
// Configure reverse control for this agent
|
|
485
|
+
if (params.reverse_control?.enabled !== false) {
|
|
486
|
+
const rcPolicies = {};
|
|
487
|
+
const samplingEvents = params.reverse_control?.sampling_events ?? ["task_broadcast", "direct_message", "subtask_completed", "bid_request_confirmation", "discussion_update"];
|
|
488
|
+
const notifEvents = params.reverse_control?.notification_events ?? ["task_collected"];
|
|
489
|
+
for (const e of samplingEvents)
|
|
490
|
+
rcPolicies[e] = { method: "sampling" };
|
|
491
|
+
for (const e of notifEvents)
|
|
492
|
+
rcPolicies[e] = { method: "notification" };
|
|
493
|
+
rcPolicies["task_timeout"] = { method: "auto_action", autoAction: "report_and_close" };
|
|
494
|
+
rc.configure(agentId, { enabled: true, policies: rcPolicies });
|
|
495
|
+
}
|
|
496
|
+
const rcStatus = rc.getStatus();
|
|
305
497
|
return ok({
|
|
306
498
|
registered: true,
|
|
307
499
|
agent_id: agentId,
|
|
@@ -309,6 +501,28 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
|
|
|
309
501
|
domains: params.domains,
|
|
310
502
|
url: agentUrl,
|
|
311
503
|
a2a_server: a2a.isRunning() ? { port: a2a.getServerPort() } : null,
|
|
504
|
+
reverse_control: {
|
|
505
|
+
enabled: params.reverse_control?.enabled !== false,
|
|
506
|
+
sampling_available: rcStatus.samplingAvailable,
|
|
507
|
+
fallback: rcStatus.samplingAvailable ? "none" : "directive_injection",
|
|
508
|
+
},
|
|
509
|
+
toolkit: {
|
|
510
|
+
workflow: {
|
|
511
|
+
eacn3_next: "Periodic dispatch loop — call on a regular interval to process pending events one at a time. Returns the highest-priority event with action directives. This is the ONLY tool that spans the full task lifecycle.",
|
|
512
|
+
eacn3_get_events: "Drain all pending events at once (bulk alternative to next).",
|
|
513
|
+
},
|
|
514
|
+
team: {
|
|
515
|
+
eacn3_team_setup: "Form a team of agents around a shared git repo via ACK message exchange.",
|
|
516
|
+
eacn3_team_status: "Check team formation progress — which ACKs are exchanged, which peer branches are known.",
|
|
517
|
+
},
|
|
518
|
+
task: {
|
|
519
|
+
eacn3_create_task: "Publish a task (supports team_id to auto-inject team preamble).",
|
|
520
|
+
eacn3_create_subtask: "Delegate part of your work as a child task.",
|
|
521
|
+
eacn3_submit_bid: "Bid on a task you want to execute.",
|
|
522
|
+
eacn3_submit_result: "Submit your completed work.",
|
|
523
|
+
eacn3_select_result: "Pick the winning result (triggers payment).",
|
|
524
|
+
},
|
|
525
|
+
},
|
|
312
526
|
});
|
|
313
527
|
});
|
|
314
528
|
// #6 eacn3_get_agent
|
|
@@ -355,11 +569,12 @@ server.tool("eacn3_update_agent", "Update a registered agent's mutable fields: n
|
|
|
355
569
|
return ok({ updated: true, agent_id, ...res });
|
|
356
570
|
});
|
|
357
571
|
// #8 eacn3_unregister_agent
|
|
358
|
-
server.tool("eacn3_unregister_agent", "Remove an agent from the network
|
|
572
|
+
server.tool("eacn3_unregister_agent", "Remove an agent from the network. Side effects: deletes agent from local state. Active tasks assigned to this agent will timeout and hurt reputation. Returns {unregistered: true, agent_id}.", {
|
|
359
573
|
agent_id: z.string(),
|
|
360
574
|
}, async (params) => {
|
|
361
575
|
const res = await net.unregisterAgent(params.agent_id);
|
|
362
|
-
|
|
576
|
+
transport.disconnect(params.agent_id);
|
|
577
|
+
rc.unconfigure(params.agent_id);
|
|
363
578
|
state.removeAgent(params.agent_id);
|
|
364
579
|
// Stop A2A server if no agents remain
|
|
365
580
|
if (state.listAgents().length === 0 && a2a.isRunning()) {
|
|
@@ -368,7 +583,7 @@ server.tool("eacn3_unregister_agent", "Remove an agent from the network and clos
|
|
|
368
583
|
return ok({ unregistered: true, agent_id: params.agent_id, ...res });
|
|
369
584
|
});
|
|
370
585
|
// #9 eacn3_list_my_agents
|
|
371
|
-
server.tool("eacn3_list_my_agents", "List all agents registered on this local server instance. Returns {count, agents[]} where each agent includes agent_id, name, domains, tier, and
|
|
586
|
+
server.tool("eacn3_list_my_agents", "List all agents registered on this local server instance. Returns {count, agents[]} where each agent includes agent_id, name, domains, tier, and registered status. No network call — reads local state only. Use to check which agents are active.", {}, async () => {
|
|
372
587
|
const agents = state.listAgents();
|
|
373
588
|
return ok({
|
|
374
589
|
count: agents.length,
|
|
@@ -376,7 +591,8 @@ server.tool("eacn3_list_my_agents", "List all agents registered on this local se
|
|
|
376
591
|
agent_id: a.agent_id,
|
|
377
592
|
name: a.name,
|
|
378
593
|
domains: a.domains,
|
|
379
|
-
|
|
594
|
+
connected: transport.isConnected(a.agent_id),
|
|
595
|
+
transport: transport.getTransportStatus(a.agent_id),
|
|
380
596
|
})),
|
|
381
597
|
});
|
|
382
598
|
});
|
|
@@ -444,6 +660,7 @@ server.tool("eacn3_list_tasks", "Browse all tasks on the network with optional f
|
|
|
444
660
|
server.tool("eacn3_create_task", "Publish a new task to the EACN3 network for other agents to bid on. Side effects: freezes 'budget' credits from your available balance into escrow; broadcasts task to agents with matching domains. Returns {task_id, status, budget, local_matches[]}. Requires: sufficient balance (use eacn3_deposit first if needed). Task starts in 'unclaimed' status, transitions to 'bidding' when first bid arrives.", {
|
|
445
661
|
description: z.string(),
|
|
446
662
|
budget: z.number(),
|
|
663
|
+
team_id: z.string().optional().describe("Team ID to attach team collaboration preamble. Required when the initiator belongs to multiple ready teams. If omitted and the initiator is in exactly one ready team, that team is used automatically."),
|
|
447
664
|
domains: z.array(z.string()).optional(),
|
|
448
665
|
deadline: z.string().optional().describe("ISO 8601 deadline"),
|
|
449
666
|
max_concurrent_bidders: z.number().optional(),
|
|
@@ -469,11 +686,48 @@ server.tool("eacn3_create_task", "Publish a new task to the EACN3 network for ot
|
|
|
469
686
|
? localAgents.filter((a) => a.agent_id !== initiatorId &&
|
|
470
687
|
params.domains.some((d) => a.domains.includes(d)))
|
|
471
688
|
: [];
|
|
472
|
-
|
|
689
|
+
// Auto-inject team collaboration preamble if initiator is in a ready team
|
|
690
|
+
let finalDescription = params.description;
|
|
691
|
+
const teams = state.getTeamsForAgent(initiatorId);
|
|
692
|
+
const readyTeams = teams.filter((t) => t.status === "ready");
|
|
693
|
+
let activeTeam;
|
|
694
|
+
if (params.team_id) {
|
|
695
|
+
activeTeam = readyTeams.find((t) => t.team_id === params.team_id);
|
|
696
|
+
if (!activeTeam) {
|
|
697
|
+
return err(`Team ${params.team_id} not found or not ready. Available ready teams: ${readyTeams.map((t) => t.team_id).join(", ") || "(none)"}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
else if (readyTeams.length === 1) {
|
|
701
|
+
activeTeam = readyTeams[0];
|
|
702
|
+
}
|
|
703
|
+
else if (readyTeams.length > 1) {
|
|
704
|
+
return err(`Initiator belongs to multiple ready teams: ${readyTeams.map((t) => t.team_id).join(", ")}. Please specify team_id.`);
|
|
705
|
+
}
|
|
706
|
+
if (activeTeam) {
|
|
707
|
+
const branchInfo = Object.entries(activeTeam.peer_branches)
|
|
708
|
+
.map(([id, branch]) => ` - ${id}: ${branch}`)
|
|
709
|
+
.join("\n");
|
|
710
|
+
const preamble = [
|
|
711
|
+
`[TEAM COLLABORATION — ${activeTeam.team_id}]`,
|
|
712
|
+
`Git repo: ${activeTeam.git_repo}`,
|
|
713
|
+
`Team members: ${activeTeam.agent_ids.join(", ")}`,
|
|
714
|
+
branchInfo ? `Branches:\n${branchInfo}` : "",
|
|
715
|
+
"",
|
|
716
|
+
"Team rules:",
|
|
717
|
+
"- We are collaborating as a team on a shared git repo. Coordinate via branches and messages.",
|
|
718
|
+
"- If any part of this task would be done better by a teammate, create a subtask (budget=0) with invited_agent_ids targeting that teammate.",
|
|
719
|
+
"- Before submitting your result, pull and check teammates' branches for relevant changes.",
|
|
720
|
+
"- Communicate progress and blockers via eacn3_send_message to your teammates.",
|
|
721
|
+
"",
|
|
722
|
+
"[USER TASK]",
|
|
723
|
+
].filter(Boolean).join("\n");
|
|
724
|
+
finalDescription = `${preamble}\n${params.description}`;
|
|
725
|
+
}
|
|
726
|
+
const task = await withAutoReconnect(() => net.createTask({
|
|
473
727
|
task_id: taskId,
|
|
474
728
|
initiator_id: initiatorId,
|
|
475
729
|
content: {
|
|
476
|
-
description:
|
|
730
|
+
description: finalDescription,
|
|
477
731
|
expected_output: params.expected_output,
|
|
478
732
|
},
|
|
479
733
|
domains: params.domains,
|
|
@@ -484,16 +738,22 @@ server.tool("eacn3_create_task", "Publish a new task to the EACN3 network for ot
|
|
|
484
738
|
human_contact: params.human_contact,
|
|
485
739
|
level: params.level ?? "general",
|
|
486
740
|
invited_agent_ids: params.invited_agent_ids,
|
|
487
|
-
});
|
|
741
|
+
}));
|
|
488
742
|
// Track locally
|
|
489
743
|
state.updateTask({
|
|
490
744
|
task_id: taskId,
|
|
745
|
+
agent_id: initiatorId,
|
|
491
746
|
role: "initiator",
|
|
492
747
|
status: task.status,
|
|
493
748
|
domains: params.domains ?? [],
|
|
494
749
|
description_summary: params.description.slice(0, 100),
|
|
495
750
|
created_at: new Date().toISOString(),
|
|
496
751
|
});
|
|
752
|
+
// If team task: reply to pending reverse handshakes with branch + task details
|
|
753
|
+
if (activeTeam) {
|
|
754
|
+
const taskSummary = { task_id: taskId, description: params.description.slice(0, 500) };
|
|
755
|
+
await replyPendingHandshakes(initiatorId, activeTeam, taskSummary);
|
|
756
|
+
}
|
|
497
757
|
return ok({
|
|
498
758
|
task_id: taskId,
|
|
499
759
|
status: task.status,
|
|
@@ -540,7 +800,7 @@ server.tool("eacn3_update_deadline", "Extend or shorten a task's deadline. Requi
|
|
|
540
800
|
return ok(res);
|
|
541
801
|
});
|
|
542
802
|
// #21 eacn3_update_discussions
|
|
543
|
-
server.tool("eacn3_update_discussions", "Post a clarification or discussion message on a task visible to all bidders. Requires: you must be the task initiator. Side effects: triggers a '
|
|
803
|
+
server.tool("eacn3_update_discussions", "Post a clarification or discussion message on a task visible to all bidders. Requires: you must be the task initiator. Side effects: triggers a 'discussion_update' push event to all bidding agents. Returns confirmation. Use to provide additional context or answer bidder questions.", {
|
|
544
804
|
task_id: z.string(),
|
|
545
805
|
message: z.string(),
|
|
546
806
|
initiator_id: z.string().optional().describe("Initiator agent ID (auto-injected if omitted)"),
|
|
@@ -550,7 +810,7 @@ server.tool("eacn3_update_discussions", "Post a clarification or discussion mess
|
|
|
550
810
|
return ok(res);
|
|
551
811
|
});
|
|
552
812
|
// #22 eacn3_confirm_budget
|
|
553
|
-
server.tool("eacn3_confirm_budget", "Approve or reject a bid that exceeded your task's budget, triggered by a '
|
|
813
|
+
server.tool("eacn3_confirm_budget", "Approve or reject a bid that exceeded your task's budget, triggered by a 'bid_request_confirmation' event. Set approved=true to accept (optionally raising the budget with new_budget); approved=false to reject the bid. Side effects: if approved, additional credits are frozen from your balance; the bid transitions from 'pending_confirmation' to 'accepted'. Returns updated task status.", {
|
|
554
814
|
task_id: z.string(),
|
|
555
815
|
approved: z.boolean(),
|
|
556
816
|
new_budget: z.number().optional(),
|
|
@@ -633,11 +893,12 @@ server.tool("eacn3_submit_bid", "Bid on an open task by specifying your confiden
|
|
|
633
893
|
const agentId = resolveAgentId(params.agent_id);
|
|
634
894
|
// Tier/level filtering and invite bypass are handled server-side in matcher.check_bid().
|
|
635
895
|
// No client-side pre-flight — the network returns "rejected" with reason for tier mismatches.
|
|
636
|
-
const res = await net.submitBid(params.task_id, agentId, params.confidence, params.price);
|
|
896
|
+
const res = await withAutoReconnect(() => net.submitBid(params.task_id, agentId, params.confidence, params.price));
|
|
637
897
|
// Track locally if not rejected (status could be "executing", "waiting_execution", etc.)
|
|
638
898
|
if (res.status && res.status !== "rejected") {
|
|
639
899
|
state.updateTask({
|
|
640
900
|
task_id: params.task_id,
|
|
901
|
+
agent_id: agentId,
|
|
641
902
|
role: "executor",
|
|
642
903
|
status: "bidding",
|
|
643
904
|
domains: [],
|
|
@@ -655,7 +916,7 @@ server.tool("eacn3_submit_result", "Submit your completed work for a task you ar
|
|
|
655
916
|
agent_id: z.string().optional().describe("Executor agent ID (auto-injected if omitted)"),
|
|
656
917
|
}, async (params) => {
|
|
657
918
|
const agentId = resolveAgentId(params.agent_id);
|
|
658
|
-
const res = await net.submitResult(params.task_id, agentId, params.content);
|
|
919
|
+
const res = await withAutoReconnect(() => net.submitResult(params.task_id, agentId, params.content));
|
|
659
920
|
// Auto-report reputation event (what logger used to do)
|
|
660
921
|
try {
|
|
661
922
|
await net.reportEvent(agentId, "task_completed");
|
|
@@ -671,7 +932,7 @@ server.tool("eacn3_reject_task", "Abandon a task you accepted, freeing your exec
|
|
|
671
932
|
agent_id: z.string().optional().describe("Executor agent ID (auto-injected if omitted)"),
|
|
672
933
|
}, async (params) => {
|
|
673
934
|
const agentId = resolveAgentId(params.agent_id);
|
|
674
|
-
const res = await net.rejectTask(params.task_id, agentId, params.reason);
|
|
935
|
+
const res = await withAutoReconnect(() => net.rejectTask(params.task_id, agentId, params.reason));
|
|
675
936
|
// Auto-report reputation event
|
|
676
937
|
try {
|
|
677
938
|
await net.reportEvent(agentId, "task_rejected");
|
|
@@ -690,7 +951,7 @@ server.tool("eacn3_create_subtask", "Delegate part of your work by creating a ch
|
|
|
690
951
|
initiator_id: z.string().optional().describe("Agent ID of the executor creating the subtask (auto-injected if omitted)"),
|
|
691
952
|
}, async (params) => {
|
|
692
953
|
const initiatorId = resolveAgentId(params.initiator_id);
|
|
693
|
-
const task = await net.createSubtask(params.parent_task_id, initiatorId, { description: params.description }, params.domains, params.budget, params.deadline, params.level);
|
|
954
|
+
const task = await withAutoReconnect(() => net.createSubtask(params.parent_task_id, initiatorId, { description: params.description }, params.domains, params.budget, params.deadline, params.level));
|
|
694
955
|
return ok({
|
|
695
956
|
subtask_id: task.id,
|
|
696
957
|
parent_task_id: params.parent_task_id,
|
|
@@ -700,7 +961,7 @@ server.tool("eacn3_create_subtask", "Delegate part of your work by creating a ch
|
|
|
700
961
|
});
|
|
701
962
|
// #27 eacn3_send_message
|
|
702
963
|
// A2A direct + Network relay fallback — agent.md:358-362
|
|
703
|
-
server.tool("eacn3_send_message", "Send a direct agent-to-agent message. Delivery order: (1) local agent → instant push, (2) remote agent with reachable URL → A2A direct POST, (3) fallback → Network relay
|
|
964
|
+
server.tool("eacn3_send_message", "Send a direct agent-to-agent message. Delivery order: (1) local agent → instant push, (2) remote agent with reachable URL → A2A direct POST, (3) fallback → Network relay. Returns {sent, to, from, method} where method is 'local', 'a2a_direct', or 'relay'. All sent messages are stored in your session history. The recipient sees a 'direct_message' event. Use /eacn3-message to handle received messages.", {
|
|
704
965
|
agent_id: z.string().describe("Target agent ID"),
|
|
705
966
|
content: z.string(),
|
|
706
967
|
sender_id: z.string().optional().describe("Your agent ID (auto-injected if omitted)"),
|
|
@@ -718,7 +979,8 @@ server.tool("eacn3_send_message", "Send a direct agent-to-agent message. Deliver
|
|
|
718
979
|
// 1. Local agent — direct push to event buffer
|
|
719
980
|
const localAgent = state.getAgent(targetId);
|
|
720
981
|
if (localAgent) {
|
|
721
|
-
state.pushEvents([{
|
|
982
|
+
state.pushEvents(targetId, [{
|
|
983
|
+
msg_id: crypto.randomUUID().replace(/-/g, ""),
|
|
722
984
|
type: "direct_message",
|
|
723
985
|
task_id: "",
|
|
724
986
|
payload: { from: senderId, content: params.content },
|
|
@@ -746,6 +1008,7 @@ server.tool("eacn3_send_message", "Send a direct agent-to-agent message. Deliver
|
|
|
746
1008
|
from: senderId,
|
|
747
1009
|
content: params.content,
|
|
748
1010
|
}),
|
|
1011
|
+
signal: AbortSignal.timeout(10_000),
|
|
749
1012
|
});
|
|
750
1013
|
if (res.ok) {
|
|
751
1014
|
return ok({ sent: true, to: targetId, from: senderId, method: "a2a_direct" });
|
|
@@ -839,24 +1102,417 @@ server.tool("eacn3_list_sessions", "List all agents you have active message sess
|
|
|
839
1102
|
});
|
|
840
1103
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
841
1104
|
// #34 eacn3_get_events
|
|
842
|
-
server.tool("eacn3_get_events", "
|
|
843
|
-
|
|
1105
|
+
server.tool("eacn3_get_events", "Fetch pending events from the network for a specific agent, plus any locally buffered synthetic events. Returns {count, events[], reverse_control} where event types include: task_broadcast, bid_request_confirmation, bid_result, discussion_update, subtask_completed, task_collected, task_timeout, adjudication_task, direct_message. With reverse_control enabled, high-priority events may already have been handled via LLM sampling — check reverse_control.status for details.", {
|
|
1106
|
+
agent_id: z.string().optional().describe("Agent ID to drain events for (auto-injected if omitted)"),
|
|
1107
|
+
}, async (params) => {
|
|
1108
|
+
const agentId = resolveAgentId(params.agent_id);
|
|
1109
|
+
const networkEvents = await transport.fetchEvents(agentId, 0);
|
|
1110
|
+
const localEvents = state.drainEvents(agentId);
|
|
1111
|
+
const events = [...networkEvents, ...localEvents].filter((e) => !e._handled);
|
|
844
1112
|
return ok({
|
|
845
1113
|
count: events.length,
|
|
846
1114
|
events,
|
|
1115
|
+
reverse_control: rc.getStatus(),
|
|
847
1116
|
});
|
|
848
1117
|
});
|
|
1118
|
+
// #39 eacn3_await_events — on-demand long-polling
|
|
1119
|
+
server.tool("eacn3_await_events", "Fetch events from the network with a configurable wait time. First checks locally buffered synthetic events, then does a single long-poll to the network. Returns {event, suggested_action, suggested_tool, suggested_params, urgency} per event, or {timeout: true}. Prefer this over eacn3_get_events for reactive agent loops.", {
|
|
1120
|
+
agent_id: z.string().optional().describe("Agent ID to await events for (auto-injected if omitted)"),
|
|
1121
|
+
timeout_seconds: z.number().optional().describe("Max seconds to wait (1-120). Default 30."),
|
|
1122
|
+
event_types: z.array(z.string()).optional().describe("Only return for these event types. Default: all."),
|
|
1123
|
+
}, async (params) => {
|
|
1124
|
+
const agentId = resolveAgentId(params.agent_id);
|
|
1125
|
+
const timeoutSec = Math.min(Math.max(params.timeout_seconds ?? 30, 1), 120);
|
|
1126
|
+
const filterTypes = params.event_types;
|
|
1127
|
+
// Check locally buffered synthetic events first
|
|
1128
|
+
const immediate = drainMatchingEvents(agentId, filterTypes).filter((e) => !e._handled);
|
|
1129
|
+
if (immediate.length > 0) {
|
|
1130
|
+
return ok(buildAwaitResponse(immediate));
|
|
1131
|
+
}
|
|
1132
|
+
// Single long-poll to network with the agent's requested timeout
|
|
1133
|
+
const networkEvents = await transport.fetchEvents(agentId, timeoutSec);
|
|
1134
|
+
const localAfter = drainMatchingEvents(agentId, filterTypes);
|
|
1135
|
+
const all = [...networkEvents, ...localAfter].filter((e) => !e._handled);
|
|
1136
|
+
// Filter by event types if specified
|
|
1137
|
+
const filtered = filterTypes && filterTypes.length > 0
|
|
1138
|
+
? all.filter((e) => filterTypes.includes(e.type))
|
|
1139
|
+
: all;
|
|
1140
|
+
// Put back non-matching events
|
|
1141
|
+
if (filterTypes && filterTypes.length > 0) {
|
|
1142
|
+
const remaining = all.filter((e) => !filterTypes.includes(e.type));
|
|
1143
|
+
if (remaining.length > 0)
|
|
1144
|
+
state.pushEvents(agentId, remaining);
|
|
1145
|
+
}
|
|
1146
|
+
if (filtered.length === 0) {
|
|
1147
|
+
return ok({ timeout: true, waited_seconds: timeoutSec, hint: "No events arrived. Call again to keep waiting, or proceed with other work." });
|
|
1148
|
+
}
|
|
1149
|
+
return ok(buildAwaitResponse(filtered));
|
|
1150
|
+
});
|
|
1151
|
+
// #41 eacn3_next — single-event non-blocking poll
|
|
1152
|
+
const URGENCY_ORDER = {
|
|
1153
|
+
task_broadcast: 1,
|
|
1154
|
+
direct_message: 1,
|
|
1155
|
+
subtask_completed: 1,
|
|
1156
|
+
bid_request_confirmation: 1,
|
|
1157
|
+
result_submitted: 1,
|
|
1158
|
+
task_collected: 2,
|
|
1159
|
+
bid_result: 2,
|
|
1160
|
+
discussion_update: 3,
|
|
1161
|
+
task_timeout: 4,
|
|
1162
|
+
adjudication_task: 2,
|
|
1163
|
+
};
|
|
1164
|
+
function buildNextAction(event) {
|
|
1165
|
+
const payload = event.payload;
|
|
1166
|
+
switch (event.type) {
|
|
1167
|
+
case "task_broadcast": {
|
|
1168
|
+
return {
|
|
1169
|
+
action: "bid",
|
|
1170
|
+
description: `New task [${(payload.domains ?? []).join(", ")}] budget=${payload.budget ?? "?"}. Evaluate and bid.`,
|
|
1171
|
+
tool: "eacn3_submit_bid",
|
|
1172
|
+
params: { task_id: event.task_id },
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
case "direct_message":
|
|
1176
|
+
return {
|
|
1177
|
+
action: "reply",
|
|
1178
|
+
description: `Message from ${payload.from ?? "?"}: "${String(payload.content ?? "").slice(0, 200)}"`,
|
|
1179
|
+
tool: "eacn3_send_message",
|
|
1180
|
+
params: { to_agent_id: payload.from, task_id: event.task_id },
|
|
1181
|
+
};
|
|
1182
|
+
case "subtask_completed":
|
|
1183
|
+
return {
|
|
1184
|
+
action: "collect",
|
|
1185
|
+
description: `Subtask ${payload.subtask_id ?? "?"} completed. Fetch results and continue.`,
|
|
1186
|
+
tool: "eacn3_get_task_results",
|
|
1187
|
+
params: { task_id: String(payload.subtask_id ?? event.task_id) },
|
|
1188
|
+
};
|
|
1189
|
+
case "bid_request_confirmation":
|
|
1190
|
+
return {
|
|
1191
|
+
action: "confirm",
|
|
1192
|
+
description: `Bid on ${event.task_id} exceeded budget. Approve or reject.`,
|
|
1193
|
+
tool: "eacn3_confirm_budget",
|
|
1194
|
+
params: { task_id: event.task_id },
|
|
1195
|
+
};
|
|
1196
|
+
case "result_submitted":
|
|
1197
|
+
return {
|
|
1198
|
+
action: "review",
|
|
1199
|
+
description: `Agent ${payload.agent_id ?? "?"} submitted a result for task ${event.task_id} (${payload.results_count ?? "?"}/${payload.executing_count ?? "?"} results in). Call eacn3_get_task_results to retrieve, then eacn3_select_result to accept${payload.all_submitted ? " — all executors have submitted" : ""}.`,
|
|
1200
|
+
tool: "eacn3_get_task_results",
|
|
1201
|
+
params: { task_id: event.task_id },
|
|
1202
|
+
};
|
|
1203
|
+
case "task_collected":
|
|
1204
|
+
return {
|
|
1205
|
+
action: "collect",
|
|
1206
|
+
description: `Task ${event.task_id}: all executors have submitted. Retrieve and select.`,
|
|
1207
|
+
tool: "eacn3_get_task_results",
|
|
1208
|
+
params: { task_id: event.task_id },
|
|
1209
|
+
};
|
|
1210
|
+
case "bid_result": {
|
|
1211
|
+
const accepted = payload?.accepted;
|
|
1212
|
+
if (accepted) {
|
|
1213
|
+
return {
|
|
1214
|
+
action: "execute",
|
|
1215
|
+
description: `Bid accepted on ${event.task_id}. Start working.`,
|
|
1216
|
+
tool: "eacn3_get_task",
|
|
1217
|
+
params: { task_id: event.task_id },
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
action: "note",
|
|
1222
|
+
description: `Bid rejected on ${event.task_id}. Reason: ${payload?.reason ?? "unknown"}.`,
|
|
1223
|
+
tool: null,
|
|
1224
|
+
params: {},
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
case "task_timeout":
|
|
1228
|
+
return {
|
|
1229
|
+
action: "note",
|
|
1230
|
+
description: `Task ${event.task_id} timed out.`,
|
|
1231
|
+
tool: null,
|
|
1232
|
+
params: {},
|
|
1233
|
+
};
|
|
1234
|
+
default:
|
|
1235
|
+
return {
|
|
1236
|
+
action: "check",
|
|
1237
|
+
description: `Event "${event.type}" on ${event.task_id}.`,
|
|
1238
|
+
tool: "eacn3_get_task",
|
|
1239
|
+
params: { task_id: event.task_id },
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
server.tool("eacn3_next", "Non-blocking single-step work dispatcher: returns the ONE highest-priority pending event for this agent with a clear action directive. When you get a task back, process it, then call eacn3_next again. When idle is returned, there are no NEW network events — but check the returned 'prompts' array for context-aware guidance: unfinished tasks, delegated work to review, reflection questions, and delegation suggestions. Act on those prompts instead of waiting. Never sleep or poll — always keep making progress.", {
|
|
1244
|
+
agent_id: z.string().optional().describe("Agent ID (auto-injected if omitted)"),
|
|
1245
|
+
}, async (params) => {
|
|
1246
|
+
const agentId = resolveAgentId(params.agent_id);
|
|
1247
|
+
// Fetch from network (non-blocking) + drain local synthetic events
|
|
1248
|
+
const networkEvents = await transport.fetchEvents(agentId, 0);
|
|
1249
|
+
const localEvents = state.drainEvents(agentId);
|
|
1250
|
+
const events = [...networkEvents, ...localEvents].filter((e) => !e._handled);
|
|
1251
|
+
if (events.length === 0) {
|
|
1252
|
+
// Build context-aware prompts based on agent's current task state
|
|
1253
|
+
const tasks = Object.values(state.getState().local_tasks).filter((t) => t.agent_id === agentId);
|
|
1254
|
+
const inProgress = tasks.filter((t) => t.role === "executor" && (t.status === "bidding" || t.status === "unclaimed"));
|
|
1255
|
+
const delegated = tasks.filter((t) => t.role === "initiator" && t.status !== "completed" && t.status !== "no_one");
|
|
1256
|
+
const completed = tasks.filter((t) => t.status === "completed" || t.status === "awaiting_retrieval");
|
|
1257
|
+
const prompts = [];
|
|
1258
|
+
if (inProgress.length > 0) {
|
|
1259
|
+
prompts.push(`You have ${inProgress.length} task(s) still in progress (${inProgress.map(t => t.task_id).join(", ")}). Have you actually finished them? Are the results thorough and correct?`);
|
|
1260
|
+
}
|
|
1261
|
+
if (delegated.length > 0) {
|
|
1262
|
+
prompts.push(`You delegated ${delegated.length} task(s) to other agents (${delegated.map(t => t.task_id).join(", ")}). Have you checked their results? Do the results meet your expectations?`);
|
|
1263
|
+
}
|
|
1264
|
+
if (completed.length > 0) {
|
|
1265
|
+
prompts.push(`You have ${completed.length} completed task(s). Have you reviewed all the results? Have you reflected on the overall outcome based on everything you've gathered so far?`);
|
|
1266
|
+
}
|
|
1267
|
+
// Check message sessions — any conversations with recent incoming messages?
|
|
1268
|
+
const sessions = state.listSessions(agentId);
|
|
1269
|
+
const unanswered = [];
|
|
1270
|
+
for (const peerId of sessions) {
|
|
1271
|
+
const msgs = state.getMessages(agentId, peerId);
|
|
1272
|
+
if (msgs.length > 0 && msgs[msgs.length - 1].direction === "in") {
|
|
1273
|
+
unanswered.push(peerId);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (unanswered.length > 0) {
|
|
1277
|
+
prompts.push(`You have unanswered messages from ${unanswered.length} agent(s) (${unanswered.join(", ")}). Have you replied? Is there information they need from you?`);
|
|
1278
|
+
}
|
|
1279
|
+
if (sessions.length > 0 && unanswered.length === 0) {
|
|
1280
|
+
prompts.push(`You have ${sessions.length} active conversation(s). Are you waiting for replies? Should you follow up?`);
|
|
1281
|
+
}
|
|
1282
|
+
if (inProgress.length === 0 && delegated.length === 0 && completed.length === 0 && sessions.length === 0) {
|
|
1283
|
+
prompts.push("No active tasks or conversations. Continue with your current work.");
|
|
1284
|
+
}
|
|
1285
|
+
// Always-applicable reflective prompts
|
|
1286
|
+
prompts.push("Are there parts of your current work that another agent with different expertise could handle better? Consider delegating via eacn3_create_task.", "If you're stuck on something, have you considered alternative approaches?", "If you have long-running subtasks, have you broken them into smaller pieces that can run in parallel?");
|
|
1287
|
+
return ok({
|
|
1288
|
+
idle: true,
|
|
1289
|
+
active_tasks: inProgress.map(t => t.task_id),
|
|
1290
|
+
delegated_tasks: delegated.map(t => t.task_id),
|
|
1291
|
+
completed_tasks: completed.map(t => t.task_id),
|
|
1292
|
+
active_conversations: sessions.length,
|
|
1293
|
+
unanswered_from: unanswered,
|
|
1294
|
+
prompts,
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
// Sort by urgency (lower number = higher priority)
|
|
1298
|
+
events.sort((a, b) => (URGENCY_ORDER[a.type] ?? 5) - (URGENCY_ORDER[b.type] ?? 5));
|
|
1299
|
+
// Take the first (highest priority), put the rest back
|
|
1300
|
+
const [top, ...rest] = events;
|
|
1301
|
+
if (rest.length > 0)
|
|
1302
|
+
state.pushEvents(agentId, rest);
|
|
1303
|
+
const next = buildNextAction(top);
|
|
1304
|
+
return ok({
|
|
1305
|
+
idle: false,
|
|
1306
|
+
remaining: rest.length,
|
|
1307
|
+
event: top,
|
|
1308
|
+
...next,
|
|
1309
|
+
});
|
|
1310
|
+
});
|
|
1311
|
+
// #42 eacn3_team_setup — task-based team handshake (fully automatic)
|
|
1312
|
+
server.tool("eacn3_team_setup", "Form a team of agents around a shared git repo. " +
|
|
1313
|
+
"Creates handshake tasks (0-budget, 30-min deadline) to exchange branch info with each peer. " +
|
|
1314
|
+
"Peers auto-bid and auto-reply; handshake is purely for branch exchange. " +
|
|
1315
|
+
"After team is ready, use eacn3_create_task with team_id to publish work for the team.", {
|
|
1316
|
+
agent_ids: z.array(z.string()).min(2).describe("Agent IDs to form a team"),
|
|
1317
|
+
git_repo: z.string().describe("Git repo URL for recording operations"),
|
|
1318
|
+
my_branch: z.string().describe("This agent's operation branch name"),
|
|
1319
|
+
}, async (params) => {
|
|
1320
|
+
// Only the calling agent creates outgoing handshakes — peers join via autoHandshakeRespond
|
|
1321
|
+
const myId = resolveAgentId(undefined);
|
|
1322
|
+
if (!state.getAgent(myId)) {
|
|
1323
|
+
return err(`Agent ${myId} is not registered on this server`);
|
|
1324
|
+
}
|
|
1325
|
+
if (!params.agent_ids.includes(myId)) {
|
|
1326
|
+
return err(`Your agent ID ${myId} must be included in agent_ids`);
|
|
1327
|
+
}
|
|
1328
|
+
const teamId = `team-${Date.now().toString(36)}`;
|
|
1329
|
+
const peers = params.agent_ids.filter((id) => id !== myId);
|
|
1330
|
+
const teamInfo = {
|
|
1331
|
+
team_id: teamId,
|
|
1332
|
+
git_repo: params.git_repo,
|
|
1333
|
+
agent_ids: params.agent_ids,
|
|
1334
|
+
my_agent_id: myId,
|
|
1335
|
+
my_branch: params.my_branch,
|
|
1336
|
+
peer_branches: {},
|
|
1337
|
+
ack_out: {},
|
|
1338
|
+
ack_in: {},
|
|
1339
|
+
is_initiator: true,
|
|
1340
|
+
status: "forming",
|
|
1341
|
+
};
|
|
1342
|
+
const tasksCreated = [];
|
|
1343
|
+
const failed = [];
|
|
1344
|
+
for (const peerId of peers) {
|
|
1345
|
+
try {
|
|
1346
|
+
const taskId = `t-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
1347
|
+
const handshakeDeadline = new Date(Date.now() + 30 * 60 * 1000).toISOString();
|
|
1348
|
+
const task = await net.createTask({
|
|
1349
|
+
task_id: taskId,
|
|
1350
|
+
initiator_id: myId,
|
|
1351
|
+
content: { description: `Team handshake: ${myId} → ${peerId} [team=${teamId}] [repo=${params.git_repo}] [members=${params.agent_ids.join(",")}]` },
|
|
1352
|
+
domains: ["team-coordination"],
|
|
1353
|
+
budget: 0,
|
|
1354
|
+
deadline: handshakeDeadline,
|
|
1355
|
+
max_concurrent_bidders: 1,
|
|
1356
|
+
max_depth: 0,
|
|
1357
|
+
invited_agent_ids: [peerId],
|
|
1358
|
+
});
|
|
1359
|
+
teamInfo.ack_out[peerId] = task.id;
|
|
1360
|
+
tasksCreated.push(task.id);
|
|
1361
|
+
state.updateTask({
|
|
1362
|
+
task_id: task.id,
|
|
1363
|
+
agent_id: myId,
|
|
1364
|
+
role: "initiator",
|
|
1365
|
+
status: task.status,
|
|
1366
|
+
domains: ["team-coordination"],
|
|
1367
|
+
description_summary: `Handshake → ${peerId}`,
|
|
1368
|
+
created_at: new Date().toISOString(),
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
catch (e) {
|
|
1372
|
+
failed.push(`${peerId}: ${e.message ?? e}`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
state.addTeam(teamInfo);
|
|
1376
|
+
return ok({
|
|
1377
|
+
team_id: teamId,
|
|
1378
|
+
git_repo: params.git_repo,
|
|
1379
|
+
agent_ids: params.agent_ids,
|
|
1380
|
+
my_agent_id: myId,
|
|
1381
|
+
my_branch: params.my_branch,
|
|
1382
|
+
tasks_created: tasksCreated,
|
|
1383
|
+
failed,
|
|
1384
|
+
next_steps: [
|
|
1385
|
+
"Handshake tasks are auto-processed — peers auto-bid and reply, results are auto-selected.",
|
|
1386
|
+
"Call eacn3_team_status to check progress. Use eacn3_team_retry_ack if a peer is unresponsive.",
|
|
1387
|
+
],
|
|
1388
|
+
});
|
|
1389
|
+
});
|
|
1390
|
+
// #43 eacn3_team_status — check team formation progress
|
|
1391
|
+
server.tool("eacn3_team_status", "Check team formation progress: which handshake tasks are complete, which peer branches are known, and whether the team is ready. If peers are unresponsive, use eacn3_team_retry_ack.", {
|
|
1392
|
+
team_id: z.string().describe("Team ID from eacn3_team_setup"),
|
|
1393
|
+
}, async (params) => {
|
|
1394
|
+
const team = state.getTeam(params.team_id);
|
|
1395
|
+
if (!team)
|
|
1396
|
+
return err(`Team ${params.team_id} not found`);
|
|
1397
|
+
const peers = team.agent_ids.filter((id) => id !== team.my_agent_id);
|
|
1398
|
+
const connected = peers.filter((id) => id in team.peer_branches);
|
|
1399
|
+
const pending = peers.filter((id) => !(id in team.peer_branches));
|
|
1400
|
+
return ok({
|
|
1401
|
+
team_id: team.team_id,
|
|
1402
|
+
git_repo: team.git_repo,
|
|
1403
|
+
status: team.status,
|
|
1404
|
+
my_agent_id: team.my_agent_id,
|
|
1405
|
+
my_branch: team.my_branch ?? null,
|
|
1406
|
+
peer_branches: team.peer_branches,
|
|
1407
|
+
ack_out: team.ack_out,
|
|
1408
|
+
ack_in: team.ack_in,
|
|
1409
|
+
connected,
|
|
1410
|
+
pending,
|
|
1411
|
+
ready: team.status === "ready",
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
// #45 eacn3_team_retry_ack — re-create handshake task for unresponsive peer
|
|
1415
|
+
server.tool("eacn3_team_retry_ack", "Re-create a handshake task for a specific peer who hasn't responded. Use when eacn3_team_status shows a peer in 'pending'.", {
|
|
1416
|
+
team_id: z.string().describe("Team ID"),
|
|
1417
|
+
peer_id: z.string().describe("Agent ID of the unresponsive peer"),
|
|
1418
|
+
}, async (params) => {
|
|
1419
|
+
const team = state.getTeam(params.team_id);
|
|
1420
|
+
if (!team)
|
|
1421
|
+
return err(`Team ${params.team_id} not found`);
|
|
1422
|
+
if (!team.agent_ids.includes(params.peer_id)) {
|
|
1423
|
+
return err(`${params.peer_id} is not a member of team ${params.team_id}`);
|
|
1424
|
+
}
|
|
1425
|
+
try {
|
|
1426
|
+
const taskId = `t-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
1427
|
+
const handshakeDeadline = new Date(Date.now() + 30 * 60 * 1000).toISOString();
|
|
1428
|
+
const task = await net.createTask({
|
|
1429
|
+
task_id: taskId,
|
|
1430
|
+
initiator_id: team.my_agent_id,
|
|
1431
|
+
content: { description: `Team handshake: ${team.my_agent_id} → ${params.peer_id} [team=${team.team_id}] [repo=${team.git_repo}] [members=${team.agent_ids.join(",")}]` },
|
|
1432
|
+
domains: ["team-coordination"],
|
|
1433
|
+
budget: 0,
|
|
1434
|
+
deadline: handshakeDeadline,
|
|
1435
|
+
max_concurrent_bidders: 1,
|
|
1436
|
+
max_depth: 0,
|
|
1437
|
+
invited_agent_ids: [params.peer_id],
|
|
1438
|
+
});
|
|
1439
|
+
team.ack_out[params.peer_id] = task.id;
|
|
1440
|
+
state.addTeam(team); // persist updated ack_out
|
|
1441
|
+
return ok({ team_id: team.team_id, peer_id: params.peer_id, task_id: task.id, message: "Handshake task re-created" });
|
|
1442
|
+
}
|
|
1443
|
+
catch (e) {
|
|
1444
|
+
return err(`Failed to create handshake task for ${params.peer_id}: ${e.message ?? e}`);
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
// #40 eacn3_reverse_control_status
|
|
1448
|
+
server.tool("eacn3_reverse_control_status", "Get the current status of the MCP reverse control engine. Shows whether sampling is available, which agents are configured, pending directive count, and rate limiting info. Use for debugging reverse control behavior.", {}, async () => {
|
|
1449
|
+
return ok(rc.getStatus());
|
|
1450
|
+
});
|
|
849
1451
|
// ---------------------------------------------------------------------------
|
|
850
|
-
//
|
|
1452
|
+
// Long-polling helpers
|
|
851
1453
|
// ---------------------------------------------------------------------------
|
|
1454
|
+
function drainMatchingEvents(agentId, filterTypes) {
|
|
1455
|
+
const all = state.drainEvents(agentId);
|
|
1456
|
+
if (!filterTypes || filterTypes.length === 0)
|
|
1457
|
+
return all;
|
|
1458
|
+
const matching = [];
|
|
1459
|
+
const remaining = [];
|
|
1460
|
+
for (const e of all) {
|
|
1461
|
+
if (filterTypes.includes(e.type)) {
|
|
1462
|
+
matching.push(e);
|
|
1463
|
+
}
|
|
1464
|
+
else {
|
|
1465
|
+
remaining.push(e);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
if (remaining.length > 0)
|
|
1469
|
+
state.pushEvents(agentId, remaining);
|
|
1470
|
+
return matching;
|
|
1471
|
+
}
|
|
1472
|
+
function buildAwaitResponse(events) {
|
|
1473
|
+
return {
|
|
1474
|
+
count: events.length,
|
|
1475
|
+
events: events.map((event) => {
|
|
1476
|
+
const payload = event.payload;
|
|
1477
|
+
switch (event.type) {
|
|
1478
|
+
case "task_broadcast":
|
|
1479
|
+
return { event, suggested_action: `New task in [${(payload.domains ?? []).join(", ")}] budget=${payload.budget ?? "?"}. Evaluate and bid.`, suggested_tool: "eacn3_submit_bid", suggested_params: { task_id: event.task_id }, urgency: "high" };
|
|
1480
|
+
case "direct_message":
|
|
1481
|
+
return { event, suggested_action: `Message from ${payload.from ?? "?"}: "${String(payload.content ?? "").slice(0, 200)}". Reply.`, suggested_tool: "eacn3_send_message", suggested_params: { to_agent_id: payload.from, task_id: event.task_id }, urgency: "high" };
|
|
1482
|
+
case "subtask_completed":
|
|
1483
|
+
return { event, suggested_action: `Subtask ${payload.subtask_id ?? "?"} done. Fetch results.`, suggested_tool: "eacn3_get_task_results", suggested_params: { task_id: String(payload.subtask_id ?? event.task_id) }, urgency: "high" };
|
|
1484
|
+
case "bid_request_confirmation":
|
|
1485
|
+
return { event, suggested_action: `Bid exceeded budget on ${event.task_id}. Approve/reject.`, suggested_tool: "eacn3_confirm_budget", suggested_params: { task_id: event.task_id }, urgency: "high" };
|
|
1486
|
+
case "result_submitted":
|
|
1487
|
+
return { event, suggested_action: `Agent ${payload.agent_id ?? "?"} submitted result for ${event.task_id}. Review and decide: select with eacn3_select_result or wait for more.`, suggested_tool: "eacn3_get_task", suggested_params: { task_id: event.task_id }, urgency: "high" };
|
|
1488
|
+
case "task_collected":
|
|
1489
|
+
return { event, suggested_action: `Task ${event.task_id}: all executors done. Retrieve and select.`, suggested_tool: "eacn3_get_task_results", suggested_params: { task_id: event.task_id }, urgency: "medium" };
|
|
1490
|
+
case "task_timeout":
|
|
1491
|
+
return { event, suggested_action: `Task ${event.task_id} timed out. No action needed.`, suggested_tool: "eacn3_get_task", suggested_params: { task_id: event.task_id }, urgency: "low" };
|
|
1492
|
+
default:
|
|
1493
|
+
return { event, suggested_action: `Event "${event.type}" on ${event.task_id}.`, suggested_tool: "eacn3_get_task", suggested_params: { task_id: event.task_id }, urgency: "low" };
|
|
1494
|
+
}
|
|
1495
|
+
}),
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
852
1498
|
// ---------------------------------------------------------------------------
|
|
853
1499
|
// WS Event Callbacks — auto-actions when events arrive
|
|
854
1500
|
// ---------------------------------------------------------------------------
|
|
855
1501
|
function registerEventCallbacks() {
|
|
856
|
-
|
|
1502
|
+
transport.setEventCallback((agentId, event) => {
|
|
1503
|
+
// Skip if agent not claimed in this session — events are on-demand only,
|
|
1504
|
+
// but guard against edge cases.
|
|
1505
|
+
if (!state.getAgent(agentId))
|
|
1506
|
+
return;
|
|
857
1507
|
const taskId = event.task_id;
|
|
1508
|
+
// --- Reverse Control: try to handle event proactively ---
|
|
1509
|
+
// This runs async; if it handles the event, it may take action
|
|
1510
|
+
// (sampling, notification, auto-action) without waiting for polling.
|
|
1511
|
+
rc.handleEvent(agentId, event).catch(() => { });
|
|
1512
|
+
// --- Legacy behavior: local state updates + event buffering ---
|
|
1513
|
+
// These still run regardless of reverse control, to keep local state consistent.
|
|
858
1514
|
switch (event.type) {
|
|
859
|
-
case "
|
|
1515
|
+
case "task_collected":
|
|
860
1516
|
// Task has results ready — update local status so dashboard/skills see it
|
|
861
1517
|
state.updateTaskStatus(taskId, "awaiting_retrieval");
|
|
862
1518
|
break;
|
|
@@ -867,7 +1523,8 @@ function registerEventCallbacks() {
|
|
|
867
1523
|
net.getTaskResults(subtaskId, agentId)
|
|
868
1524
|
.then((res) => {
|
|
869
1525
|
// Buffer a synthetic event with the results for the skill to pick up
|
|
870
|
-
state.pushEvents([{
|
|
1526
|
+
state.pushEvents(agentId, [{
|
|
1527
|
+
msg_id: crypto.randomUUID().replace(/-/g, ""),
|
|
871
1528
|
type: "subtask_completed",
|
|
872
1529
|
task_id: taskId,
|
|
873
1530
|
payload: { subtask_id: subtaskId, results: res.results },
|
|
@@ -878,21 +1535,48 @@ function registerEventCallbacks() {
|
|
|
878
1535
|
}
|
|
879
1536
|
break;
|
|
880
1537
|
}
|
|
881
|
-
case "
|
|
1538
|
+
case "task_timeout":
|
|
882
1539
|
// Task timed out — auto-report reputation event, update local status
|
|
883
1540
|
state.updateTaskStatus(taskId, "no_one");
|
|
884
1541
|
net.reportEvent(agentId, "task_timeout").catch(() => { });
|
|
885
1542
|
break;
|
|
886
|
-
case "
|
|
1543
|
+
case "bid_request_confirmation":
|
|
887
1544
|
// Bid exceeded budget — mark in local state for initiator to handle
|
|
888
1545
|
// The event stays in the buffer for /eacn3-bounty to surface
|
|
889
1546
|
break;
|
|
890
|
-
case "task_broadcast":
|
|
891
|
-
|
|
892
|
-
|
|
1547
|
+
case "task_broadcast": {
|
|
1548
|
+
const bPayload = event.payload;
|
|
1549
|
+
const bDomains = bPayload?.domains ?? [];
|
|
1550
|
+
const bDesc = String(bPayload?.description ?? "");
|
|
1551
|
+
if (bDomains.includes("team-coordination") && bDesc.startsWith("Team handshake:")) {
|
|
1552
|
+
event._handled = true;
|
|
1553
|
+
autoHandshakeRespond(agentId, event).catch((e) => { console.error(`[handshake] autoHandshakeRespond failed for ${agentId}:`, e); });
|
|
1554
|
+
}
|
|
1555
|
+
else {
|
|
1556
|
+
autoBidEvaluate(agentId, event).catch(() => { });
|
|
1557
|
+
}
|
|
1558
|
+
break;
|
|
1559
|
+
}
|
|
1560
|
+
case "bid_result": {
|
|
1561
|
+
const brPayload = event.payload;
|
|
1562
|
+
if (brPayload?.accepted) {
|
|
1563
|
+
const match = state.findTeamByHandshakeTask(event.task_id);
|
|
1564
|
+
if (match && match.direction === "in") {
|
|
1565
|
+
event._handled = true;
|
|
1566
|
+
autoHandshakeSubmit(agentId, event).catch((e) => { console.error(`[handshake] autoHandshakeSubmit failed for ${agentId}:`, e); });
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
break;
|
|
1570
|
+
}
|
|
1571
|
+
case "result_submitted": {
|
|
1572
|
+
const match = state.findTeamByHandshakeTask(event.task_id);
|
|
1573
|
+
if (match && match.direction === "out") {
|
|
1574
|
+
event._handled = true;
|
|
1575
|
+
autoHandshakeSelect(agentId, event).catch((e) => { console.error(`[handshake] autoHandshakeSelect failed for ${agentId}:`, e); });
|
|
1576
|
+
}
|
|
893
1577
|
break;
|
|
1578
|
+
}
|
|
894
1579
|
case "direct_message": {
|
|
895
|
-
// Another agent sent a direct message — store in session
|
|
896
1580
|
const payload = event.payload;
|
|
897
1581
|
const from = payload?.from;
|
|
898
1582
|
const content = payload?.content;
|
|
@@ -911,6 +1595,198 @@ function registerEventCallbacks() {
|
|
|
911
1595
|
});
|
|
912
1596
|
}
|
|
913
1597
|
// ---------------------------------------------------------------------------
|
|
1598
|
+
// Team handshake auto-handling
|
|
1599
|
+
// ---------------------------------------------------------------------------
|
|
1600
|
+
/**
|
|
1601
|
+
* Auto-respond to an incoming handshake task_broadcast:
|
|
1602
|
+
* 1. Parse team info from description
|
|
1603
|
+
* 2. Create local team record if needed, auto-set branch to agent/{id}
|
|
1604
|
+
* 3. Auto-bid on the incoming task
|
|
1605
|
+
* 4. Create outgoing handshake tasks to all peers we haven't ACKed yet
|
|
1606
|
+
*/
|
|
1607
|
+
async function autoHandshakeRespond(agentId, event) {
|
|
1608
|
+
const payload = event.payload;
|
|
1609
|
+
const desc = String(payload?.description ?? "");
|
|
1610
|
+
const teamMatch = desc.match(/\[team=([^\]]+)\]/);
|
|
1611
|
+
const repoMatch = desc.match(/\[repo=([^\]]+)\]/);
|
|
1612
|
+
const membersMatch = desc.match(/\[members=([^\]]+)\]/);
|
|
1613
|
+
const fromMatch = desc.match(/^Team handshake:\s*(\S+)/);
|
|
1614
|
+
if (!teamMatch)
|
|
1615
|
+
return;
|
|
1616
|
+
const teamId = teamMatch[1];
|
|
1617
|
+
const gitRepo = repoMatch?.[1] ?? "";
|
|
1618
|
+
const members = membersMatch?.[1]?.split(",") ?? [];
|
|
1619
|
+
const fromAgent = fromMatch?.[1] ?? "";
|
|
1620
|
+
// Ensure local team record exists
|
|
1621
|
+
let team = state.getTeamsForAgent(agentId).find((t) => t.team_id === teamId);
|
|
1622
|
+
if (!team) {
|
|
1623
|
+
const teamInfo = {
|
|
1624
|
+
team_id: teamId,
|
|
1625
|
+
git_repo: gitRepo,
|
|
1626
|
+
agent_ids: members,
|
|
1627
|
+
my_agent_id: agentId,
|
|
1628
|
+
my_branch: `agent/${agentId}`,
|
|
1629
|
+
peer_branches: {},
|
|
1630
|
+
ack_out: {},
|
|
1631
|
+
ack_in: {},
|
|
1632
|
+
status: "forming",
|
|
1633
|
+
};
|
|
1634
|
+
state.addTeam(teamInfo);
|
|
1635
|
+
team = state.getTeamsForAgent(agentId).find((t) => t.team_id === teamId);
|
|
1636
|
+
}
|
|
1637
|
+
// Auto-set branch if not yet set
|
|
1638
|
+
if (!team.my_branch) {
|
|
1639
|
+
state.setTeamBranch(teamId, `agent/${agentId}`);
|
|
1640
|
+
team.my_branch = `agent/${agentId}`;
|
|
1641
|
+
}
|
|
1642
|
+
// Record incoming handshake task
|
|
1643
|
+
state.recordAckIn(teamId, agentId, fromAgent, event.task_id);
|
|
1644
|
+
// If this agent is the team initiator (called eacn3_team_setup), DON'T auto-respond.
|
|
1645
|
+
// The initiator replies later via replyPendingHandshakes (called by create_task)
|
|
1646
|
+
// so it can include task details in the response.
|
|
1647
|
+
if (team.is_initiator)
|
|
1648
|
+
return;
|
|
1649
|
+
// Auto-bid on the incoming task
|
|
1650
|
+
try {
|
|
1651
|
+
await net.submitBid(event.task_id, agentId, 0, 1);
|
|
1652
|
+
}
|
|
1653
|
+
catch (e) {
|
|
1654
|
+
console.error(`[handshake] auto-bid failed for ${agentId} on task ${event.task_id}:`, e);
|
|
1655
|
+
return; // Can't proceed without a bid
|
|
1656
|
+
}
|
|
1657
|
+
// Create outgoing tasks to peers we haven't ACKed yet
|
|
1658
|
+
await createOutgoingHandshakes(team, agentId);
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* Auto-submit result after handshake bid is accepted (bid_result event).
|
|
1662
|
+
* Refuses to submit if branch is not set.
|
|
1663
|
+
*/
|
|
1664
|
+
async function autoHandshakeSubmit(agentId, event) {
|
|
1665
|
+
const taskId = event.task_id;
|
|
1666
|
+
const match = state.findTeamByHandshakeTask(taskId);
|
|
1667
|
+
if (!match || match.direction !== "in")
|
|
1668
|
+
return;
|
|
1669
|
+
try {
|
|
1670
|
+
await net.submitResult(taskId, agentId, {
|
|
1671
|
+
_handshake_ack: true,
|
|
1672
|
+
team_id: match.team.team_id,
|
|
1673
|
+
branch: match.team.my_branch ?? `agent/${agentId}`,
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
catch (e) {
|
|
1677
|
+
console.error(`[handshake] auto-submit failed for ${agentId} on task ${taskId}:`, e);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Create outgoing handshake tasks to all peers we haven't ACKed yet.
|
|
1682
|
+
*/
|
|
1683
|
+
async function createOutgoingHandshakes(team, agentId) {
|
|
1684
|
+
const peers = team.agent_ids.filter((id) => id !== agentId);
|
|
1685
|
+
for (const peerId of peers) {
|
|
1686
|
+
if (team.ack_out[peerId])
|
|
1687
|
+
continue;
|
|
1688
|
+
try {
|
|
1689
|
+
const taskId = `t-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
1690
|
+
const handshakeDeadline = new Date(Date.now() + 30 * 60 * 1000).toISOString();
|
|
1691
|
+
const task = await net.createTask({
|
|
1692
|
+
task_id: taskId,
|
|
1693
|
+
initiator_id: agentId,
|
|
1694
|
+
content: { description: `Team handshake: ${agentId} → ${peerId} [team=${team.team_id}] [repo=${team.git_repo}] [members=${team.agent_ids.join(",")}]` },
|
|
1695
|
+
domains: ["team-coordination"],
|
|
1696
|
+
budget: 0,
|
|
1697
|
+
deadline: handshakeDeadline,
|
|
1698
|
+
max_concurrent_bidders: 1,
|
|
1699
|
+
max_depth: 0,
|
|
1700
|
+
invited_agent_ids: [peerId],
|
|
1701
|
+
});
|
|
1702
|
+
team.ack_out[peerId] = task.id;
|
|
1703
|
+
state.addTeam(team);
|
|
1704
|
+
}
|
|
1705
|
+
catch (e) {
|
|
1706
|
+
console.error(`[handshake] createOutgoingHandshake ${agentId} → ${peerId} failed:`, e);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* Auto-select result for handshake tasks when a result is submitted.
|
|
1712
|
+
* Only acts on tasks in our ack_out (i.e., tasks we created as initiator).
|
|
1713
|
+
*/
|
|
1714
|
+
async function autoHandshakeSelect(agentId, event) {
|
|
1715
|
+
const taskId = event.task_id;
|
|
1716
|
+
const match = state.findTeamByHandshakeTask(taskId);
|
|
1717
|
+
if (!match || match.direction !== "out")
|
|
1718
|
+
return; // Not our outgoing handshake
|
|
1719
|
+
const payload = event.payload;
|
|
1720
|
+
const resultAgentId = payload?.agent_id;
|
|
1721
|
+
if (!resultAgentId)
|
|
1722
|
+
return;
|
|
1723
|
+
// Auto-select the result
|
|
1724
|
+
try {
|
|
1725
|
+
await net.selectResult(taskId, agentId, resultAgentId);
|
|
1726
|
+
}
|
|
1727
|
+
catch (e) {
|
|
1728
|
+
console.error(`[handshake] auto-select failed for ${agentId} on task ${taskId}:`, e);
|
|
1729
|
+
}
|
|
1730
|
+
// Extract branch from the result
|
|
1731
|
+
try {
|
|
1732
|
+
const taskData = await net.getTask(taskId);
|
|
1733
|
+
const results = taskData?.results ?? [];
|
|
1734
|
+
const result = results.find((r) => r.agent_id === resultAgentId);
|
|
1735
|
+
const branch = result?.content?.branch;
|
|
1736
|
+
if (branch) {
|
|
1737
|
+
state.updateTeamPeerBranch(match.team.team_id, match.peerId, branch);
|
|
1738
|
+
}
|
|
1739
|
+
// If the reply carries a team task, buffer it as a synthetic event for the agent
|
|
1740
|
+
const teamTask = result?.content?.team_task;
|
|
1741
|
+
if (teamTask && teamTask.task_id) {
|
|
1742
|
+
state.pushEvents(agentId, [{
|
|
1743
|
+
msg_id: crypto.randomUUID().replace(/-/g, ""),
|
|
1744
|
+
type: "direct_message",
|
|
1745
|
+
task_id: teamTask.task_id,
|
|
1746
|
+
payload: {
|
|
1747
|
+
from: match.peerId,
|
|
1748
|
+
content: JSON.stringify({ _team_task: true, ...teamTask }),
|
|
1749
|
+
},
|
|
1750
|
+
received_at: Date.now(),
|
|
1751
|
+
}]);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
catch (e) {
|
|
1755
|
+
console.error(`[handshake] branch extraction failed for task ${taskId}:`, e);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Reply to pending reverse handshakes when creating a team task.
|
|
1760
|
+
* Uses ack_in (recorded by autoHandshakeRespond) to find task IDs
|
|
1761
|
+
* that the initiator hasn't responded to yet. Bids and submits result
|
|
1762
|
+
* with branch + task details.
|
|
1763
|
+
*/
|
|
1764
|
+
async function replyPendingHandshakes(agentId, team, taskSummary) {
|
|
1765
|
+
for (const [peerId, taskId] of Object.entries(team.ack_in)) {
|
|
1766
|
+
// Bid on the reverse handshake task
|
|
1767
|
+
try {
|
|
1768
|
+
await net.submitBid(taskId, agentId, 0, 1);
|
|
1769
|
+
}
|
|
1770
|
+
catch (e) {
|
|
1771
|
+
console.error(`[handshake] replyPendingHandshakes bid failed for ${agentId} → ${peerId} (task ${taskId}):`, e);
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
// Submit result with branch + task details
|
|
1775
|
+
// On a 0-budget invited task, bid is typically auto-accepted
|
|
1776
|
+
try {
|
|
1777
|
+
await net.submitResult(taskId, agentId, {
|
|
1778
|
+
_handshake_ack: true,
|
|
1779
|
+
team_id: team.team_id,
|
|
1780
|
+
branch: team.my_branch ?? `agent/${agentId}`,
|
|
1781
|
+
team_task: taskSummary,
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
catch (e) {
|
|
1785
|
+
console.error(`[handshake] replyPendingHandshakes submit failed for ${agentId} → ${peerId} (task ${taskId}):`, e);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
// ---------------------------------------------------------------------------
|
|
914
1790
|
// Auto-bid evaluation — communication layer auto-filter per agent.md:172-193
|
|
915
1791
|
// ---------------------------------------------------------------------------
|
|
916
1792
|
async function autoBidEvaluate(agentId, event) {
|
|
@@ -932,7 +1808,8 @@ async function autoBidEvaluate(agentId, event) {
|
|
|
932
1808
|
}
|
|
933
1809
|
// Passed auto-filter — enrich the buffered event with a hint
|
|
934
1810
|
// The skill layer (/eacn3-bounty) will see this and can fast-track bidding
|
|
935
|
-
state.pushEvents([{
|
|
1811
|
+
state.pushEvents(agentId, [{
|
|
1812
|
+
msg_id: crypto.randomUUID().replace(/-/g, ""),
|
|
936
1813
|
type: "task_broadcast",
|
|
937
1814
|
task_id: taskId,
|
|
938
1815
|
payload: { ...payload, auto_match: true, matched_agent: agentId },
|
|
@@ -940,6 +1817,29 @@ async function autoBidEvaluate(agentId, event) {
|
|
|
940
1817
|
}]);
|
|
941
1818
|
}
|
|
942
1819
|
// ---------------------------------------------------------------------------
|
|
1820
|
+
// Global crash handlers — log to file so post-mortem is possible
|
|
1821
|
+
// ---------------------------------------------------------------------------
|
|
1822
|
+
const __crash_dir = join(dirname(fileURLToPath(import.meta.url)), "..", "logs");
|
|
1823
|
+
function writeCrashLog(label, err) {
|
|
1824
|
+
try {
|
|
1825
|
+
mkdirSync(__crash_dir, { recursive: true });
|
|
1826
|
+
const ts = new Date().toISOString();
|
|
1827
|
+
const msg = err instanceof Error ? `${err.message}\n${err.stack}` : String(err);
|
|
1828
|
+
const line = `[${ts}] ${label}: ${msg}\n`;
|
|
1829
|
+
appendFileSync(join(__crash_dir, "crash.log"), line);
|
|
1830
|
+
console.error(`[EACN3] ${label}:`, msg);
|
|
1831
|
+
}
|
|
1832
|
+
catch { /* last resort — nothing we can do */ }
|
|
1833
|
+
}
|
|
1834
|
+
process.on("uncaughtException", (err) => {
|
|
1835
|
+
writeCrashLog("uncaughtException", err);
|
|
1836
|
+
process.exit(1);
|
|
1837
|
+
});
|
|
1838
|
+
process.on("unhandledRejection", (reason) => {
|
|
1839
|
+
writeCrashLog("unhandledRejection", reason);
|
|
1840
|
+
process.exit(1);
|
|
1841
|
+
});
|
|
1842
|
+
// ---------------------------------------------------------------------------
|
|
943
1843
|
// Start
|
|
944
1844
|
// ---------------------------------------------------------------------------
|
|
945
1845
|
async function main() {
|
|
@@ -949,6 +1849,9 @@ async function main() {
|
|
|
949
1849
|
registerEventCallbacks();
|
|
950
1850
|
const transport = new StdioServerTransport();
|
|
951
1851
|
await server.connect(transport);
|
|
1852
|
+
// Initialize reverse control engine with the underlying MCP Server instance.
|
|
1853
|
+
// Must be called AFTER connect() so client capabilities are available.
|
|
1854
|
+
rc.init(server.server ?? server);
|
|
952
1855
|
}
|
|
953
1856
|
main().catch((e) => {
|
|
954
1857
|
console.error("EACN3 MCP server failed to start:", e);
|