eacn3 0.3.3 → 0.5.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/dist/index.d.ts +7 -1
- package/dist/index.js +917 -620
- package/dist/index.js.map +1 -1
- package/dist/server.js +639 -47
- 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 +31 -0
- package/dist/src/event-transport.js +178 -0
- package/dist/src/event-transport.js.map +1 -0
- package/dist/src/models.d.ts +55 -13
- package/dist/src/models.js +1 -1
- package/dist/src/models.js.map +1 -1
- package/dist/src/network-client.d.ts +1 -1
- package/dist/src/network-client.js +4 -5
- 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 +17 -4
- package/dist/src/state.js +134 -12
- package/dist/src/state.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -6
- package/scripts/cli.cjs +13 -12
- 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-browse/SKILL.md +1 -1
- 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-register/SKILL.md +3 -15
- package/skills/eacn3-register-zh/SKILL.md +3 -15
- package/skills/eacn3-task/SKILL.md +1 -1
- package/skills/eacn3-task-zh/SKILL.md +1 -1
package/dist/server.js
CHANGED
|
@@ -11,13 +11,24 @@ 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 ws from "./src/
|
|
14
|
+
import * as ws from "./src/event-transport.js";
|
|
15
15
|
import * as a2a from "./src/a2a-server.js";
|
|
16
|
+
import * as rc from "./src/reverse-control.js";
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Helper: MCP text result
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
function ok(data) {
|
|
20
|
-
|
|
21
|
+
const result = {
|
|
22
|
+
content: [{ type: "text", text: JSON.stringify(data) }],
|
|
23
|
+
};
|
|
24
|
+
// Fallback directive injection: when sampling is unavailable,
|
|
25
|
+
// append pending event directives to any tool response so the
|
|
26
|
+
// Host LLM sees actionable events without explicit polling.
|
|
27
|
+
const directives = rc.drainDirectives();
|
|
28
|
+
if (directives) {
|
|
29
|
+
result.content.push({ type: "text", text: directives });
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
21
32
|
}
|
|
22
33
|
function err(message) {
|
|
23
34
|
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }] };
|
|
@@ -70,7 +81,7 @@ function stopHeartbeat() {
|
|
|
70
81
|
// ---------------------------------------------------------------------------
|
|
71
82
|
// MCP Server
|
|
72
83
|
// ---------------------------------------------------------------------------
|
|
73
|
-
const server = new McpServer({ name: "eacn3", version: "0.
|
|
84
|
+
const server = new McpServer({ name: "eacn3", version: "0.5.0" });
|
|
74
85
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
75
86
|
// Health / Cluster (2)
|
|
76
87
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -104,7 +115,7 @@ server.tool("eacn3_cluster_status", "Retrieve the full cluster topology includin
|
|
|
104
115
|
// Server Management (4)
|
|
105
116
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
106
117
|
// #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, agents_online}. Side effects:
|
|
118
|
+
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, agents_online, restored_agents, hint}. Side effects: starts event polling for any previously registered agents. IMPORTANT: check restored_agents in the response — if you have previously registered agents, they are already reconnected and ready to use. You do NOT need to re-register them. Only call eacn3_register_agent if you need a NEW agent.", {
|
|
108
119
|
network_endpoint: z.string().optional().describe(`Network URL. Defaults to ${EACN3_DEFAULT_NETWORK_ENDPOINT}`),
|
|
109
120
|
seed_nodes: z.array(z.string()).optional().describe("Additional seed node URLs for fallback"),
|
|
110
121
|
}, async (params) => {
|
|
@@ -121,41 +132,110 @@ server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your
|
|
|
121
132
|
return err(`Cannot reach any network node: ${e.message}`);
|
|
122
133
|
}
|
|
123
134
|
s.network_endpoint = endpoint;
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
s.server_card
|
|
127
|
-
server_id
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
// Reuse existing server identity if available; otherwise register new
|
|
136
|
+
let sid;
|
|
137
|
+
if (s.server_card) {
|
|
138
|
+
// Try to reconnect with existing server_id via heartbeat
|
|
139
|
+
try {
|
|
140
|
+
await net.heartbeat();
|
|
141
|
+
sid = s.server_card.server_id;
|
|
142
|
+
s.server_card.status = "online";
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Server no longer known to network — re-register
|
|
146
|
+
const res = await net.registerServer("0.5.0", "plugin://local", "plugin-user");
|
|
147
|
+
sid = res.server_id;
|
|
148
|
+
s.server_card = {
|
|
149
|
+
server_id: sid,
|
|
150
|
+
version: "0.5.0",
|
|
151
|
+
endpoint: "plugin://local",
|
|
152
|
+
owner: "plugin-user",
|
|
153
|
+
status: "online",
|
|
154
|
+
};
|
|
155
|
+
// Update server_id on all persisted agents and re-register them with the network
|
|
156
|
+
for (const agent of Object.values(s.agents)) {
|
|
157
|
+
agent.server_id = sid;
|
|
158
|
+
try {
|
|
159
|
+
await net.registerAgent(agent);
|
|
160
|
+
}
|
|
161
|
+
catch { /* best-effort */ }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const res = await net.registerServer("0.5.0", "plugin://local", "plugin-user");
|
|
167
|
+
sid = res.server_id;
|
|
168
|
+
s.server_card = {
|
|
169
|
+
server_id: sid,
|
|
170
|
+
version: "0.5.0",
|
|
171
|
+
endpoint: "plugin://local",
|
|
172
|
+
owner: "plugin-user",
|
|
173
|
+
status: "online",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
133
176
|
state.save();
|
|
134
177
|
// Start background heartbeat
|
|
135
178
|
startHeartbeat();
|
|
136
|
-
// Reconnect WS for all existing agents
|
|
137
|
-
for (const
|
|
138
|
-
|
|
179
|
+
// Reconnect WS for all existing agents; re-register if network lost them
|
|
180
|
+
for (const agent of Object.values(s.agents)) {
|
|
181
|
+
try {
|
|
182
|
+
await net.getAgentInfo(agent.agent_id);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Agent not found on network (e.g. server restarted with in-memory DB)
|
|
186
|
+
try {
|
|
187
|
+
await net.registerAgent(agent);
|
|
188
|
+
}
|
|
189
|
+
catch { /* best-effort */ }
|
|
190
|
+
}
|
|
191
|
+
ws.connect(agent.agent_id);
|
|
139
192
|
}
|
|
193
|
+
const restoredAgents = Object.values(s.agents).map((a) => ({
|
|
194
|
+
agent_id: a.agent_id,
|
|
195
|
+
name: a.name,
|
|
196
|
+
domains: a.domains,
|
|
197
|
+
tier: a.tier,
|
|
198
|
+
}));
|
|
140
199
|
return ok({
|
|
141
200
|
connected: true,
|
|
142
|
-
server_id:
|
|
201
|
+
server_id: sid,
|
|
143
202
|
network_endpoint: endpoint,
|
|
144
203
|
fallback,
|
|
145
|
-
agents_online:
|
|
204
|
+
agents_online: restoredAgents.length,
|
|
205
|
+
restored_agents: restoredAgents,
|
|
206
|
+
hint: restoredAgents.length > 0
|
|
207
|
+
? "You have previously registered agents restored and reconnected. You can use them directly without re-registering. Call eacn3_list_my_agents() for full details."
|
|
208
|
+
: "No previous agents found. Register a new agent with eacn3_register_agent().",
|
|
209
|
+
toolkit: {
|
|
210
|
+
workflow: {
|
|
211
|
+
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.",
|
|
212
|
+
eacn3_get_events: "Drain all pending events at once (bulk alternative to next).",
|
|
213
|
+
},
|
|
214
|
+
team: {
|
|
215
|
+
eacn3_team_setup: "Form a team of agents around a shared git repo. Creates handshake tasks between all pairs.",
|
|
216
|
+
eacn3_team_status: "Check team formation progress — which handshakes are done, which peer branches are known.",
|
|
217
|
+
eacn3_team_set_branch: "Record your git branch name for a team after creating it.",
|
|
218
|
+
},
|
|
219
|
+
task: {
|
|
220
|
+
eacn3_create_task: "Publish a task (supports team_id to auto-inject team preamble).",
|
|
221
|
+
eacn3_create_subtask: "Delegate part of your work as a child task.",
|
|
222
|
+
eacn3_submit_bid: "Bid on a task you want to execute.",
|
|
223
|
+
eacn3_submit_result: "Submit your completed work.",
|
|
224
|
+
eacn3_select_result: "Pick the winning result (triggers payment).",
|
|
225
|
+
},
|
|
226
|
+
},
|
|
146
227
|
});
|
|
147
228
|
});
|
|
148
229
|
// #2 eacn3_disconnect
|
|
149
|
-
server.tool("eacn3_disconnect", "Disconnect from the EACN3 network
|
|
230
|
+
server.tool("eacn3_disconnect", "Disconnect from the EACN3 network and stop event polling. Requires: eacn3_connect first. Side effects: active tasks will timeout and hurt reputation. Server identity and agent registrations are preserved — on next eacn3_connect they will be automatically reconnected. Returns {disconnected: true}. Only call at end of session.", {}, async () => {
|
|
150
231
|
stopHeartbeat();
|
|
151
232
|
ws.disconnectAll();
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
catch { /* may already be gone */ }
|
|
233
|
+
// Do NOT call unregisterServer — it cascade-deletes all agents on the network side.
|
|
234
|
+
// We only go offline; identity is preserved for reconnection.
|
|
156
235
|
const s = state.getState();
|
|
157
|
-
s.server_card
|
|
158
|
-
|
|
236
|
+
if (s.server_card) {
|
|
237
|
+
s.server_card.status = "offline";
|
|
238
|
+
}
|
|
159
239
|
state.save();
|
|
160
240
|
return ok({ disconnected: true });
|
|
161
241
|
});
|
|
@@ -190,7 +270,7 @@ server.tool("eacn3_server_info", "Get current server connection state, including
|
|
|
190
270
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
191
271
|
// #5 eacn3_register_agent
|
|
192
272
|
// Inlines: adapter (AgentCard assembly) + registry (validate + persist + DHT)
|
|
193
|
-
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
|
|
273
|
+
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').", {
|
|
194
274
|
name: z.string().describe("Agent display name"),
|
|
195
275
|
description: z.string().describe("What this Agent does"),
|
|
196
276
|
domains: z.array(z.string()).describe("Capability domains (e.g. ['translation', 'coding'])"),
|
|
@@ -205,11 +285,15 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
|
|
|
205
285
|
max_concurrent_tasks: z.number().describe("Max tasks this Agent can handle simultaneously (0 = unlimited)"),
|
|
206
286
|
concurrent: z.boolean().describe("Whether this Agent supports concurrent execution"),
|
|
207
287
|
}).optional().describe("Agent capacity limits"),
|
|
208
|
-
agent_type: z.enum(["executor", "planner"]).optional().describe("Defaults to executor"),
|
|
209
288
|
tier: z.enum(["general", "expert", "expert_general", "tool"]).optional().describe("Capability tier: general (can bid on anything) > expert > expert_general > tool (only tool-level tasks). Defaults to general."),
|
|
210
289
|
agent_id: z.string().optional().describe("Custom agent ID. Auto-generated if omitted."),
|
|
211
290
|
a2a_port: z.number().optional().describe("Port for A2A HTTP server. Enables direct agent-to-agent messaging. Omit to use Network relay only."),
|
|
212
291
|
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."),
|
|
292
|
+
reverse_control: z.object({
|
|
293
|
+
enabled: z.boolean().optional().describe("Enable MCP reverse control (sampling/notifications). Default true."),
|
|
294
|
+
sampling_events: z.array(z.string()).optional().describe("Event types that trigger LLM sampling (e.g. ['task_broadcast', 'direct_message']). Default: all actionable events."),
|
|
295
|
+
notification_events: z.array(z.string()).optional().describe("Event types that send notifications only (e.g. ['task_collected', 'task_timeout']). Default: status events."),
|
|
296
|
+
}).optional().describe("Configure MCP reverse control — lets the network proactively drive your agent via sampling requests."),
|
|
213
297
|
}, async (params) => {
|
|
214
298
|
const s = state.getState();
|
|
215
299
|
if (!s.server_card)
|
|
@@ -237,7 +321,6 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
|
|
|
237
321
|
const card = {
|
|
238
322
|
agent_id: agentId,
|
|
239
323
|
name: params.name,
|
|
240
|
-
agent_type: params.agent_type ?? "executor",
|
|
241
324
|
tier: params.tier ?? "general",
|
|
242
325
|
domains: params.domains,
|
|
243
326
|
skills: params.skills ?? [],
|
|
@@ -251,8 +334,21 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
|
|
|
251
334
|
const res = await net.registerAgent(card);
|
|
252
335
|
// Persist locally
|
|
253
336
|
state.addAgent(card);
|
|
254
|
-
//
|
|
337
|
+
// Start polling for push events
|
|
255
338
|
ws.connect(agentId);
|
|
339
|
+
// Configure reverse control for this agent
|
|
340
|
+
if (params.reverse_control?.enabled !== false) {
|
|
341
|
+
const rcPolicies = {};
|
|
342
|
+
const samplingEvents = params.reverse_control?.sampling_events ?? ["task_broadcast", "direct_message", "subtask_completed", "bid_request_confirmation", "discussion_update"];
|
|
343
|
+
const notifEvents = params.reverse_control?.notification_events ?? ["task_collected"];
|
|
344
|
+
for (const e of samplingEvents)
|
|
345
|
+
rcPolicies[e] = { method: "sampling" };
|
|
346
|
+
for (const e of notifEvents)
|
|
347
|
+
rcPolicies[e] = { method: "notification" };
|
|
348
|
+
rcPolicies["task_timeout"] = { method: "auto_action", autoAction: "report_and_close" };
|
|
349
|
+
rc.configure(agentId, { enabled: true, policies: rcPolicies });
|
|
350
|
+
}
|
|
351
|
+
const rcStatus = rc.getStatus();
|
|
256
352
|
return ok({
|
|
257
353
|
registered: true,
|
|
258
354
|
agent_id: agentId,
|
|
@@ -260,10 +356,33 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
|
|
|
260
356
|
domains: params.domains,
|
|
261
357
|
url: agentUrl,
|
|
262
358
|
a2a_server: a2a.isRunning() ? { port: a2a.getServerPort() } : null,
|
|
359
|
+
reverse_control: {
|
|
360
|
+
enabled: params.reverse_control?.enabled !== false,
|
|
361
|
+
sampling_available: rcStatus.samplingAvailable,
|
|
362
|
+
fallback: rcStatus.samplingAvailable ? "none" : "directive_injection",
|
|
363
|
+
},
|
|
364
|
+
toolkit: {
|
|
365
|
+
workflow: {
|
|
366
|
+
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.",
|
|
367
|
+
eacn3_get_events: "Drain all pending events at once (bulk alternative to next).",
|
|
368
|
+
},
|
|
369
|
+
team: {
|
|
370
|
+
eacn3_team_setup: "Form a team of agents around a shared git repo. Creates handshake tasks between all pairs.",
|
|
371
|
+
eacn3_team_status: "Check team formation progress — which handshakes are done, which peer branches are known.",
|
|
372
|
+
eacn3_team_set_branch: "Record your git branch name for a team after creating it.",
|
|
373
|
+
},
|
|
374
|
+
task: {
|
|
375
|
+
eacn3_create_task: "Publish a task (supports team_id to auto-inject team preamble).",
|
|
376
|
+
eacn3_create_subtask: "Delegate part of your work as a child task.",
|
|
377
|
+
eacn3_submit_bid: "Bid on a task you want to execute.",
|
|
378
|
+
eacn3_submit_result: "Submit your completed work.",
|
|
379
|
+
eacn3_select_result: "Pick the winning result (triggers payment).",
|
|
380
|
+
},
|
|
381
|
+
},
|
|
263
382
|
});
|
|
264
383
|
});
|
|
265
384
|
// #6 eacn3_get_agent
|
|
266
|
-
server.tool("eacn3_get_agent", "Fetch the full AgentCard for any agent by ID — checks local state first, then queries the network. Returns {agent_id, name,
|
|
385
|
+
server.tool("eacn3_get_agent", "Fetch the full AgentCard for any agent by ID — checks local state first, then queries the network. Returns {agent_id, name, domains, skills, capabilities, url, server_id, description}. No side effects. Use to inspect an agent before sending messages or evaluating bids.", {
|
|
267
386
|
agent_id: z.string(),
|
|
268
387
|
}, async (params) => {
|
|
269
388
|
// Check local first
|
|
@@ -306,11 +425,12 @@ server.tool("eacn3_update_agent", "Update a registered agent's mutable fields: n
|
|
|
306
425
|
return ok({ updated: true, agent_id, ...res });
|
|
307
426
|
});
|
|
308
427
|
// #8 eacn3_unregister_agent
|
|
309
|
-
server.tool("eacn3_unregister_agent", "Remove an agent from the network and
|
|
428
|
+
server.tool("eacn3_unregister_agent", "Remove an agent from the network and stop its event polling. Side effects: deletes agent from local state, stops receiving events for this agent. Active tasks assigned to this agent will timeout and hurt reputation. Returns {unregistered: true, agent_id}.", {
|
|
310
429
|
agent_id: z.string(),
|
|
311
430
|
}, async (params) => {
|
|
312
431
|
const res = await net.unregisterAgent(params.agent_id);
|
|
313
432
|
ws.disconnect(params.agent_id);
|
|
433
|
+
rc.unconfigure(params.agent_id);
|
|
314
434
|
state.removeAgent(params.agent_id);
|
|
315
435
|
// Stop A2A server if no agents remain
|
|
316
436
|
if (state.listAgents().length === 0 && a2a.isRunning()) {
|
|
@@ -319,16 +439,16 @@ server.tool("eacn3_unregister_agent", "Remove an agent from the network and clos
|
|
|
319
439
|
return ok({ unregistered: true, agent_id: params.agent_id, ...res });
|
|
320
440
|
});
|
|
321
441
|
// #9 eacn3_list_my_agents
|
|
322
|
-
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,
|
|
442
|
+
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 polling_active (event polling status). No network call — reads local state only. Use to check which agents are active and receiving events.", {}, async () => {
|
|
323
443
|
const agents = state.listAgents();
|
|
324
444
|
return ok({
|
|
325
445
|
count: agents.length,
|
|
326
446
|
agents: agents.map((a) => ({
|
|
327
447
|
agent_id: a.agent_id,
|
|
328
448
|
name: a.name,
|
|
329
|
-
agent_type: a.agent_type,
|
|
330
449
|
domains: a.domains,
|
|
331
|
-
|
|
450
|
+
connected: ws.isConnected(a.agent_id),
|
|
451
|
+
transport: ws.getTransportStatus(a.agent_id),
|
|
332
452
|
})),
|
|
333
453
|
});
|
|
334
454
|
});
|
|
@@ -396,6 +516,7 @@ server.tool("eacn3_list_tasks", "Browse all tasks on the network with optional f
|
|
|
396
516
|
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.", {
|
|
397
517
|
description: z.string(),
|
|
398
518
|
budget: z.number(),
|
|
519
|
+
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."),
|
|
399
520
|
domains: z.array(z.string()).optional(),
|
|
400
521
|
deadline: z.string().optional().describe("ISO 8601 deadline"),
|
|
401
522
|
max_concurrent_bidders: z.number().optional(),
|
|
@@ -421,11 +542,48 @@ server.tool("eacn3_create_task", "Publish a new task to the EACN3 network for ot
|
|
|
421
542
|
? localAgents.filter((a) => a.agent_id !== initiatorId &&
|
|
422
543
|
params.domains.some((d) => a.domains.includes(d)))
|
|
423
544
|
: [];
|
|
545
|
+
// Auto-inject team collaboration preamble if initiator is in a ready team
|
|
546
|
+
let finalDescription = params.description;
|
|
547
|
+
const teams = state.getTeamsForAgent(initiatorId);
|
|
548
|
+
const readyTeams = teams.filter((t) => t.status === "ready");
|
|
549
|
+
let activeTeam;
|
|
550
|
+
if (params.team_id) {
|
|
551
|
+
activeTeam = readyTeams.find((t) => t.team_id === params.team_id);
|
|
552
|
+
if (!activeTeam) {
|
|
553
|
+
return err(`Team ${params.team_id} not found or not ready. Available ready teams: ${readyTeams.map((t) => t.team_id).join(", ") || "(none)"}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
else if (readyTeams.length === 1) {
|
|
557
|
+
activeTeam = readyTeams[0];
|
|
558
|
+
}
|
|
559
|
+
else if (readyTeams.length > 1) {
|
|
560
|
+
return err(`Initiator belongs to multiple ready teams: ${readyTeams.map((t) => t.team_id).join(", ")}. Please specify team_id.`);
|
|
561
|
+
}
|
|
562
|
+
if (activeTeam) {
|
|
563
|
+
const branchInfo = Object.entries(activeTeam.peer_branches)
|
|
564
|
+
.map(([id, branch]) => ` - ${id}: ${branch}`)
|
|
565
|
+
.join("\n");
|
|
566
|
+
const preamble = [
|
|
567
|
+
`[TEAM COLLABORATION — ${activeTeam.team_id}]`,
|
|
568
|
+
`Git repo: ${activeTeam.git_repo}`,
|
|
569
|
+
`Team members: ${activeTeam.agent_ids.join(", ")}`,
|
|
570
|
+
branchInfo ? `Branches:\n${branchInfo}` : "",
|
|
571
|
+
"",
|
|
572
|
+
"Team rules:",
|
|
573
|
+
"- We are collaborating as a team on a shared git repo. Coordinate via branches and messages.",
|
|
574
|
+
"- 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.",
|
|
575
|
+
"- Before submitting your result, pull and check teammates' branches for relevant changes.",
|
|
576
|
+
"- Communicate progress and blockers via eacn3_send_message to your teammates.",
|
|
577
|
+
"",
|
|
578
|
+
"[USER TASK]",
|
|
579
|
+
].filter(Boolean).join("\n");
|
|
580
|
+
finalDescription = `${preamble}\n${params.description}`;
|
|
581
|
+
}
|
|
424
582
|
const task = await net.createTask({
|
|
425
583
|
task_id: taskId,
|
|
426
584
|
initiator_id: initiatorId,
|
|
427
585
|
content: {
|
|
428
|
-
description:
|
|
586
|
+
description: finalDescription,
|
|
429
587
|
expected_output: params.expected_output,
|
|
430
588
|
},
|
|
431
589
|
domains: params.domains,
|
|
@@ -440,6 +598,7 @@ server.tool("eacn3_create_task", "Publish a new task to the EACN3 network for ot
|
|
|
440
598
|
// Track locally
|
|
441
599
|
state.updateTask({
|
|
442
600
|
task_id: taskId,
|
|
601
|
+
agent_id: initiatorId,
|
|
443
602
|
role: "initiator",
|
|
444
603
|
status: task.status,
|
|
445
604
|
domains: params.domains ?? [],
|
|
@@ -470,6 +629,25 @@ server.tool("eacn3_select_result", "Pick the winning result for a task, triggeri
|
|
|
470
629
|
}, async (params) => {
|
|
471
630
|
const initiatorId = resolveAgentId(params.initiator_id);
|
|
472
631
|
const res = await net.selectResult(params.task_id, initiatorId, params.agent_id);
|
|
632
|
+
// If this was a team handshake task, extract peer branch and update local team state
|
|
633
|
+
const teams = state.getTeamsForAgent(initiatorId);
|
|
634
|
+
for (const team of teams) {
|
|
635
|
+
const handshakeTarget = Object.entries(team.handshake_out).find(([, taskId]) => taskId === params.task_id);
|
|
636
|
+
if (handshakeTarget) {
|
|
637
|
+
// Fetch the result to get the branch name
|
|
638
|
+
try {
|
|
639
|
+
const taskData = await net.getTask(params.task_id);
|
|
640
|
+
const results = taskData?.results ?? [];
|
|
641
|
+
const winnerResult = results.find((r) => r.agent_id === params.agent_id);
|
|
642
|
+
const branch = winnerResult?.content?.branch;
|
|
643
|
+
if (branch) {
|
|
644
|
+
state.updateTeamPeerBranch(team.team_id, handshakeTarget[0], branch);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch { /* best effort */ }
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
473
651
|
return ok(res);
|
|
474
652
|
});
|
|
475
653
|
// #19 eacn3_close_task
|
|
@@ -492,7 +670,7 @@ server.tool("eacn3_update_deadline", "Extend or shorten a task's deadline. Requi
|
|
|
492
670
|
return ok(res);
|
|
493
671
|
});
|
|
494
672
|
// #21 eacn3_update_discussions
|
|
495
|
-
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 '
|
|
673
|
+
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.", {
|
|
496
674
|
task_id: z.string(),
|
|
497
675
|
message: z.string(),
|
|
498
676
|
initiator_id: z.string().optional().describe("Initiator agent ID (auto-injected if omitted)"),
|
|
@@ -502,7 +680,7 @@ server.tool("eacn3_update_discussions", "Post a clarification or discussion mess
|
|
|
502
680
|
return ok(res);
|
|
503
681
|
});
|
|
504
682
|
// #22 eacn3_confirm_budget
|
|
505
|
-
server.tool("eacn3_confirm_budget", "Approve or reject a bid that exceeded your task's budget, triggered by a '
|
|
683
|
+
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.", {
|
|
506
684
|
task_id: z.string(),
|
|
507
685
|
approved: z.boolean(),
|
|
508
686
|
new_budget: z.number().optional(),
|
|
@@ -590,6 +768,7 @@ server.tool("eacn3_submit_bid", "Bid on an open task by specifying your confiden
|
|
|
590
768
|
if (res.status && res.status !== "rejected") {
|
|
591
769
|
state.updateTask({
|
|
592
770
|
task_id: params.task_id,
|
|
771
|
+
agent_id: agentId,
|
|
593
772
|
role: "executor",
|
|
594
773
|
status: "bidding",
|
|
595
774
|
domains: [],
|
|
@@ -652,7 +831,7 @@ server.tool("eacn3_create_subtask", "Delegate part of your work by creating a ch
|
|
|
652
831
|
});
|
|
653
832
|
// #27 eacn3_send_message
|
|
654
833
|
// A2A direct + Network relay fallback — agent.md:358-362
|
|
655
|
-
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
|
|
834
|
+
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.", {
|
|
656
835
|
agent_id: z.string().describe("Target agent ID"),
|
|
657
836
|
content: z.string(),
|
|
658
837
|
sender_id: z.string().optional().describe("Your agent ID (auto-injected if omitted)"),
|
|
@@ -670,7 +849,8 @@ server.tool("eacn3_send_message", "Send a direct agent-to-agent message. Deliver
|
|
|
670
849
|
// 1. Local agent — direct push to event buffer
|
|
671
850
|
const localAgent = state.getAgent(targetId);
|
|
672
851
|
if (localAgent) {
|
|
673
|
-
state.pushEvents([{
|
|
852
|
+
state.pushEvents(targetId, [{
|
|
853
|
+
msg_id: crypto.randomUUID().replace(/-/g, ""),
|
|
674
854
|
type: "direct_message",
|
|
675
855
|
task_id: "",
|
|
676
856
|
payload: { from: senderId, content: params.content },
|
|
@@ -791,24 +971,431 @@ server.tool("eacn3_list_sessions", "List all agents you have active message sess
|
|
|
791
971
|
});
|
|
792
972
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
793
973
|
// #34 eacn3_get_events
|
|
794
|
-
server.tool("eacn3_get_events", "Drain the in-memory event buffer, returning
|
|
795
|
-
|
|
974
|
+
server.tool("eacn3_get_events", "Drain the in-memory event buffer for a specific agent, returning its pending events and clearing them. 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. Call periodically in your main loop.", {
|
|
975
|
+
agent_id: z.string().optional().describe("Agent ID to drain events for (auto-injected if omitted)"),
|
|
976
|
+
}, async (params) => {
|
|
977
|
+
const agentId = resolveAgentId(params.agent_id);
|
|
978
|
+
const events = state.drainEvents(agentId);
|
|
796
979
|
return ok({
|
|
797
980
|
count: events.length,
|
|
798
981
|
events,
|
|
982
|
+
reverse_control: rc.getStatus(),
|
|
799
983
|
});
|
|
800
984
|
});
|
|
985
|
+
// #39 eacn3_await_events — long-polling reverse control
|
|
986
|
+
server.tool("eacn3_await_events", "Block until a network event arrives or timeout expires, then return with the event AND a suggested action. This is the reverse-control mechanism when MCP sampling is unavailable (e.g. OpenClaw). Instead of polling eacn3_get_events in a loop, call this — it waits for the network to push something, then tells you exactly what to do. Returns {event, suggested_action, suggested_tool, suggested_params, urgency} per event, or {timeout: true}. Prefer this over eacn3_get_events for reactive agent loops.", {
|
|
987
|
+
agent_id: z.string().optional().describe("Agent ID to await events for (auto-injected if omitted)"),
|
|
988
|
+
timeout_seconds: z.number().optional().describe("Max seconds to wait (1-120). Default 30."),
|
|
989
|
+
event_types: z.array(z.string()).optional().describe("Only return for these event types. Default: all."),
|
|
990
|
+
}, async (params) => {
|
|
991
|
+
const agentId = resolveAgentId(params.agent_id);
|
|
992
|
+
const timeoutSec = Math.min(Math.max(params.timeout_seconds ?? 30, 1), 120);
|
|
993
|
+
const filterTypes = params.event_types;
|
|
994
|
+
// Check immediate buffered events
|
|
995
|
+
const immediate = drainMatchingEvents(agentId, filterTypes);
|
|
996
|
+
if (immediate.length > 0) {
|
|
997
|
+
return ok(buildAwaitResponse(immediate));
|
|
998
|
+
}
|
|
999
|
+
// Long-poll
|
|
1000
|
+
const result = await new Promise((resolve) => {
|
|
1001
|
+
const deadline = setTimeout(() => { cleanup(); resolve([]); }, timeoutSec * 1000);
|
|
1002
|
+
const poll = setInterval(() => {
|
|
1003
|
+
const events = drainMatchingEvents(agentId, filterTypes);
|
|
1004
|
+
if (events.length > 0) {
|
|
1005
|
+
cleanup();
|
|
1006
|
+
resolve(events);
|
|
1007
|
+
}
|
|
1008
|
+
}, 500);
|
|
1009
|
+
function cleanup() { clearTimeout(deadline); clearInterval(poll); }
|
|
1010
|
+
});
|
|
1011
|
+
if (result.length === 0) {
|
|
1012
|
+
return ok({ timeout: true, waited_seconds: timeoutSec, hint: "No events arrived. Call again to keep waiting, or proceed with other work." });
|
|
1013
|
+
}
|
|
1014
|
+
return ok(buildAwaitResponse(result));
|
|
1015
|
+
});
|
|
1016
|
+
// #41 eacn3_next — single-event non-blocking poll
|
|
1017
|
+
const URGENCY_ORDER = {
|
|
1018
|
+
task_broadcast: 1,
|
|
1019
|
+
direct_message: 1,
|
|
1020
|
+
subtask_completed: 1,
|
|
1021
|
+
bid_request_confirmation: 1,
|
|
1022
|
+
result_submitted: 1,
|
|
1023
|
+
task_collected: 2,
|
|
1024
|
+
bid_result: 2,
|
|
1025
|
+
discussion_update: 3,
|
|
1026
|
+
task_timeout: 4,
|
|
1027
|
+
adjudication_task: 2,
|
|
1028
|
+
};
|
|
1029
|
+
function buildNextAction(event) {
|
|
1030
|
+
const payload = event.payload;
|
|
1031
|
+
switch (event.type) {
|
|
1032
|
+
case "task_broadcast": {
|
|
1033
|
+
// Detect team handshake tasks
|
|
1034
|
+
const desc = String(payload?.description ?? "");
|
|
1035
|
+
if (desc.startsWith("Team handshake:") || payload?.budget === 0) {
|
|
1036
|
+
// Check if this looks like a handshake (0-budget, team-coordination domain)
|
|
1037
|
+
const domains = payload.domains ?? [];
|
|
1038
|
+
if (domains.includes("team-coordination")) {
|
|
1039
|
+
return {
|
|
1040
|
+
action: "handshake",
|
|
1041
|
+
description: `Team handshake from another agent on task ${event.task_id}. ` +
|
|
1042
|
+
`Bid with price=0 confidence=1, then submit_result with your branch name: ` +
|
|
1043
|
+
`{branch: "agent/your-id", _handshake_ack: true, team_id: "..."}.`,
|
|
1044
|
+
tool: "eacn3_submit_bid",
|
|
1045
|
+
params: { task_id: event.task_id, confidence: 1, price: 0 },
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return {
|
|
1050
|
+
action: "bid",
|
|
1051
|
+
description: `New task [${(payload.domains ?? []).join(", ")}] budget=${payload.budget ?? "?"}. Evaluate and bid.`,
|
|
1052
|
+
tool: "eacn3_submit_bid",
|
|
1053
|
+
params: { task_id: event.task_id },
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
case "direct_message":
|
|
1057
|
+
return {
|
|
1058
|
+
action: "reply",
|
|
1059
|
+
description: `Message from ${payload.from ?? "?"}: "${String(payload.content ?? "").slice(0, 200)}"`,
|
|
1060
|
+
tool: "eacn3_send_message",
|
|
1061
|
+
params: { to_agent_id: payload.from, task_id: event.task_id },
|
|
1062
|
+
};
|
|
1063
|
+
case "subtask_completed":
|
|
1064
|
+
return {
|
|
1065
|
+
action: "collect",
|
|
1066
|
+
description: `Subtask ${payload.subtask_id ?? "?"} completed. Fetch results and continue.`,
|
|
1067
|
+
tool: "eacn3_get_task_results",
|
|
1068
|
+
params: { task_id: String(payload.subtask_id ?? event.task_id) },
|
|
1069
|
+
};
|
|
1070
|
+
case "bid_request_confirmation":
|
|
1071
|
+
return {
|
|
1072
|
+
action: "confirm",
|
|
1073
|
+
description: `Bid on ${event.task_id} exceeded budget. Approve or reject.`,
|
|
1074
|
+
tool: "eacn3_confirm_budget",
|
|
1075
|
+
params: { task_id: event.task_id },
|
|
1076
|
+
};
|
|
1077
|
+
case "result_submitted":
|
|
1078
|
+
return {
|
|
1079
|
+
action: "review",
|
|
1080
|
+
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" : ""}.`,
|
|
1081
|
+
tool: "eacn3_get_task_results",
|
|
1082
|
+
params: { task_id: event.task_id },
|
|
1083
|
+
};
|
|
1084
|
+
case "task_collected":
|
|
1085
|
+
return {
|
|
1086
|
+
action: "collect",
|
|
1087
|
+
description: `Task ${event.task_id}: all executors have submitted. Retrieve and select.`,
|
|
1088
|
+
tool: "eacn3_get_task_results",
|
|
1089
|
+
params: { task_id: event.task_id },
|
|
1090
|
+
};
|
|
1091
|
+
case "bid_result": {
|
|
1092
|
+
const accepted = payload?.accepted;
|
|
1093
|
+
if (accepted) {
|
|
1094
|
+
return {
|
|
1095
|
+
action: "execute",
|
|
1096
|
+
description: `Bid accepted on ${event.task_id}. Start working.`,
|
|
1097
|
+
tool: "eacn3_get_task",
|
|
1098
|
+
params: { task_id: event.task_id },
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
action: "note",
|
|
1103
|
+
description: `Bid rejected on ${event.task_id}. Reason: ${payload?.reason ?? "unknown"}.`,
|
|
1104
|
+
tool: null,
|
|
1105
|
+
params: {},
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
case "task_timeout":
|
|
1109
|
+
return {
|
|
1110
|
+
action: "note",
|
|
1111
|
+
description: `Task ${event.task_id} timed out.`,
|
|
1112
|
+
tool: null,
|
|
1113
|
+
params: {},
|
|
1114
|
+
};
|
|
1115
|
+
default:
|
|
1116
|
+
return {
|
|
1117
|
+
action: "check",
|
|
1118
|
+
description: `Event "${event.type}" on ${event.task_id}.`,
|
|
1119
|
+
tool: "eacn3_get_task",
|
|
1120
|
+
params: { task_id: event.task_id },
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
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.", {
|
|
1125
|
+
agent_id: z.string().optional().describe("Agent ID (auto-injected if omitted)"),
|
|
1126
|
+
}, async (params) => {
|
|
1127
|
+
const agentId = resolveAgentId(params.agent_id);
|
|
1128
|
+
const events = state.drainEvents(agentId);
|
|
1129
|
+
if (events.length === 0) {
|
|
1130
|
+
// Build context-aware prompts based on agent's current task state
|
|
1131
|
+
const tasks = Object.values(state.getState().local_tasks).filter((t) => t.agent_id === agentId);
|
|
1132
|
+
const inProgress = tasks.filter((t) => t.role === "executor" && (t.status === "bidding" || t.status === "unclaimed"));
|
|
1133
|
+
const delegated = tasks.filter((t) => t.role === "initiator" && t.status !== "completed" && t.status !== "no_one");
|
|
1134
|
+
const completed = tasks.filter((t) => t.status === "completed" || t.status === "awaiting_retrieval");
|
|
1135
|
+
const prompts = [];
|
|
1136
|
+
if (inProgress.length > 0) {
|
|
1137
|
+
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?`);
|
|
1138
|
+
}
|
|
1139
|
+
if (delegated.length > 0) {
|
|
1140
|
+
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?`);
|
|
1141
|
+
}
|
|
1142
|
+
if (completed.length > 0) {
|
|
1143
|
+
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?`);
|
|
1144
|
+
}
|
|
1145
|
+
// Check message sessions — any conversations with recent incoming messages?
|
|
1146
|
+
const sessions = state.listSessions(agentId);
|
|
1147
|
+
const unanswered = [];
|
|
1148
|
+
for (const peerId of sessions) {
|
|
1149
|
+
const msgs = state.getMessages(agentId, peerId);
|
|
1150
|
+
if (msgs.length > 0 && msgs[msgs.length - 1].direction === "in") {
|
|
1151
|
+
unanswered.push(peerId);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (unanswered.length > 0) {
|
|
1155
|
+
prompts.push(`You have unanswered messages from ${unanswered.length} agent(s) (${unanswered.join(", ")}). Have you replied? Is there information they need from you?`);
|
|
1156
|
+
}
|
|
1157
|
+
if (sessions.length > 0 && unanswered.length === 0) {
|
|
1158
|
+
prompts.push(`You have ${sessions.length} active conversation(s). Are you waiting for replies? Should you follow up?`);
|
|
1159
|
+
}
|
|
1160
|
+
if (inProgress.length === 0 && delegated.length === 0 && completed.length === 0 && sessions.length === 0) {
|
|
1161
|
+
prompts.push("No active tasks or conversations. Continue with your current work.");
|
|
1162
|
+
}
|
|
1163
|
+
// Always-applicable reflective prompts
|
|
1164
|
+
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?");
|
|
1165
|
+
return ok({
|
|
1166
|
+
idle: true,
|
|
1167
|
+
active_tasks: inProgress.map(t => t.task_id),
|
|
1168
|
+
delegated_tasks: delegated.map(t => t.task_id),
|
|
1169
|
+
completed_tasks: completed.map(t => t.task_id),
|
|
1170
|
+
active_conversations: sessions.length,
|
|
1171
|
+
unanswered_from: unanswered,
|
|
1172
|
+
prompts,
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
// Sort by urgency (lower number = higher priority)
|
|
1176
|
+
events.sort((a, b) => (URGENCY_ORDER[a.type] ?? 5) - (URGENCY_ORDER[b.type] ?? 5));
|
|
1177
|
+
// Take the first (highest priority), put the rest back
|
|
1178
|
+
const [top, ...rest] = events;
|
|
1179
|
+
if (rest.length > 0)
|
|
1180
|
+
state.pushEvents(agentId, rest);
|
|
1181
|
+
const next = buildNextAction(top);
|
|
1182
|
+
return ok({
|
|
1183
|
+
idle: false,
|
|
1184
|
+
remaining: rest.length,
|
|
1185
|
+
event: top,
|
|
1186
|
+
...next,
|
|
1187
|
+
});
|
|
1188
|
+
});
|
|
1189
|
+
// #42 eacn3_team_setup — human-facing team coordination toolkit
|
|
1190
|
+
server.tool("eacn3_team_setup", "Human-facing toolkit: form a team of agents around a shared git repo. " +
|
|
1191
|
+
"Creates 0-budget handshake tasks between every pair of agents (both directions). " +
|
|
1192
|
+
"Each agent creates its own operation branch and learns peers' branches through handshake responses. " +
|
|
1193
|
+
"Set peers_running_next=true if other agents already have next loops — this agent will notify them via messages first.", {
|
|
1194
|
+
agent_ids: z.array(z.string()).min(2).describe("Agent IDs to form a team"),
|
|
1195
|
+
git_repo: z.string().describe("Git repo URL for recording operations"),
|
|
1196
|
+
peers_running_next: z.boolean().optional().describe("If true, notify peers via messages before handshakes"),
|
|
1197
|
+
}, async (params) => {
|
|
1198
|
+
const localAgents = state.listAgents();
|
|
1199
|
+
const localIds = new Set(localAgents.map((a) => a.agent_id));
|
|
1200
|
+
// Find which of the requested agents are local
|
|
1201
|
+
const myAgents = params.agent_ids.filter((id) => localIds.has(id));
|
|
1202
|
+
if (myAgents.length === 0) {
|
|
1203
|
+
return err("None of the specified agent_ids are registered on this server");
|
|
1204
|
+
}
|
|
1205
|
+
const teamId = `team-${Date.now().toString(36)}`;
|
|
1206
|
+
const results = [];
|
|
1207
|
+
for (const myId of myAgents) {
|
|
1208
|
+
const peers = params.agent_ids.filter((id) => id !== myId);
|
|
1209
|
+
// Save team info
|
|
1210
|
+
const teamInfo = {
|
|
1211
|
+
team_id: teamId,
|
|
1212
|
+
git_repo: params.git_repo,
|
|
1213
|
+
agent_ids: params.agent_ids,
|
|
1214
|
+
my_agent_id: myId,
|
|
1215
|
+
peer_branches: {},
|
|
1216
|
+
handshake_out: {},
|
|
1217
|
+
handshake_in: {},
|
|
1218
|
+
status: "forming",
|
|
1219
|
+
};
|
|
1220
|
+
// If peers are running next loops, notify them first
|
|
1221
|
+
if (params.peers_running_next) {
|
|
1222
|
+
for (const peerId of peers) {
|
|
1223
|
+
try {
|
|
1224
|
+
const msg = JSON.stringify({
|
|
1225
|
+
_team_notify: true,
|
|
1226
|
+
team_id: teamId,
|
|
1227
|
+
git_repo: params.git_repo,
|
|
1228
|
+
team_members: params.agent_ids,
|
|
1229
|
+
message: `Team setup initiated. You will receive handshake tasks shortly. Create your branch in ${params.git_repo} and respond with your branch name.`,
|
|
1230
|
+
});
|
|
1231
|
+
await net.relayMessage({
|
|
1232
|
+
to: { network_id: "", server_id: "", agent_id: peerId },
|
|
1233
|
+
from: { network_id: "", server_id: state.getServerId() ?? "", agent_id: myId },
|
|
1234
|
+
content: msg,
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
catch { /* best-effort notification */ }
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
// Create 0-budget handshake task for each peer
|
|
1241
|
+
const handshakesCreated = [];
|
|
1242
|
+
for (const peerId of peers) {
|
|
1243
|
+
try {
|
|
1244
|
+
const taskId = `t-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
1245
|
+
const content = {
|
|
1246
|
+
_handshake: true,
|
|
1247
|
+
team_id: teamId,
|
|
1248
|
+
git_repo: params.git_repo,
|
|
1249
|
+
from_agent: myId,
|
|
1250
|
+
team_members: params.agent_ids,
|
|
1251
|
+
};
|
|
1252
|
+
const task = await net.createTask({
|
|
1253
|
+
task_id: taskId,
|
|
1254
|
+
initiator_id: myId,
|
|
1255
|
+
content: { description: `Team handshake: ${myId} → ${peerId}` },
|
|
1256
|
+
domains: ["team-coordination"],
|
|
1257
|
+
budget: 0,
|
|
1258
|
+
max_concurrent_bidders: 1,
|
|
1259
|
+
max_depth: 0,
|
|
1260
|
+
invited_agent_ids: [peerId],
|
|
1261
|
+
});
|
|
1262
|
+
// Update task content with handshake marker via discussions
|
|
1263
|
+
try {
|
|
1264
|
+
await net.updateDiscussions(task.id, myId, JSON.stringify(content));
|
|
1265
|
+
}
|
|
1266
|
+
catch { /* content is in description as fallback */ }
|
|
1267
|
+
teamInfo.handshake_out[peerId] = task.id;
|
|
1268
|
+
handshakesCreated.push(task.id);
|
|
1269
|
+
state.updateTask({
|
|
1270
|
+
task_id: task.id,
|
|
1271
|
+
agent_id: myId,
|
|
1272
|
+
role: "initiator",
|
|
1273
|
+
status: task.status,
|
|
1274
|
+
domains: ["team-coordination"],
|
|
1275
|
+
description_summary: `Handshake → ${peerId}`,
|
|
1276
|
+
created_at: new Date().toISOString(),
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
catch (e) {
|
|
1280
|
+
handshakesCreated.push(`FAILED(${peerId}): ${e.message ?? e}`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
state.addTeam(teamInfo);
|
|
1284
|
+
results.push({ agent_id: myId, handshakes_created: handshakesCreated });
|
|
1285
|
+
}
|
|
1286
|
+
return ok({
|
|
1287
|
+
team_id: teamId,
|
|
1288
|
+
git_repo: params.git_repo,
|
|
1289
|
+
agent_ids: params.agent_ids,
|
|
1290
|
+
local_agents: myAgents,
|
|
1291
|
+
results,
|
|
1292
|
+
next_steps: [
|
|
1293
|
+
"Each agent should now create their operation branch in the git repo.",
|
|
1294
|
+
"When receiving handshake tasks (task_broadcast with _handshake marker), " +
|
|
1295
|
+
"bid with price=0 and confidence=1, then submit_result with {branch: 'your-branch-name', _handshake_ack: true, team_id: '...'}.",
|
|
1296
|
+
"When your handshake tasks get results, select_result to complete the handshake.",
|
|
1297
|
+
"Team is ready when all peer branches are known.",
|
|
1298
|
+
],
|
|
1299
|
+
});
|
|
1300
|
+
});
|
|
1301
|
+
// #43 eacn3_team_status — check team formation progress
|
|
1302
|
+
server.tool("eacn3_team_status", "Check the current status of a team: which handshakes are complete, which peer branches are known, and whether the team is ready.", {
|
|
1303
|
+
team_id: z.string().describe("Team ID from eacn3_team_setup"),
|
|
1304
|
+
}, async (params) => {
|
|
1305
|
+
const team = state.getTeam(params.team_id);
|
|
1306
|
+
if (!team)
|
|
1307
|
+
return err(`Team ${params.team_id} not found`);
|
|
1308
|
+
const peers = team.agent_ids.filter((id) => id !== team.my_agent_id);
|
|
1309
|
+
const handshakes_complete = peers.filter((id) => id in team.peer_branches);
|
|
1310
|
+
const handshakes_pending = peers.filter((id) => !(id in team.peer_branches));
|
|
1311
|
+
return ok({
|
|
1312
|
+
team_id: team.team_id,
|
|
1313
|
+
git_repo: team.git_repo,
|
|
1314
|
+
status: team.status,
|
|
1315
|
+
my_agent_id: team.my_agent_id,
|
|
1316
|
+
my_branch: team.my_branch ?? null,
|
|
1317
|
+
peer_branches: team.peer_branches,
|
|
1318
|
+
handshakes_complete: handshakes_complete,
|
|
1319
|
+
handshakes_pending: handshakes_pending,
|
|
1320
|
+
ready: team.status === "ready",
|
|
1321
|
+
});
|
|
1322
|
+
});
|
|
1323
|
+
// #44 eacn3_team_set_branch — record this agent's operation branch
|
|
1324
|
+
server.tool("eacn3_team_set_branch", "Record this agent's git branch name for a team. Call after creating your branch in the git repo.", {
|
|
1325
|
+
team_id: z.string().describe("Team ID"),
|
|
1326
|
+
branch: z.string().describe("Branch name (e.g. 'agent/my-agent-id')"),
|
|
1327
|
+
}, async (params) => {
|
|
1328
|
+
const team = state.getTeam(params.team_id);
|
|
1329
|
+
if (!team)
|
|
1330
|
+
return err(`Team ${params.team_id} not found`);
|
|
1331
|
+
state.setTeamBranch(params.team_id, params.branch);
|
|
1332
|
+
return ok({ team_id: params.team_id, branch: params.branch, message: "Branch recorded" });
|
|
1333
|
+
});
|
|
1334
|
+
// #40 eacn3_reverse_control_status
|
|
1335
|
+
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 () => {
|
|
1336
|
+
return ok(rc.getStatus());
|
|
1337
|
+
});
|
|
801
1338
|
// ---------------------------------------------------------------------------
|
|
802
|
-
//
|
|
1339
|
+
// Long-polling helpers
|
|
803
1340
|
// ---------------------------------------------------------------------------
|
|
1341
|
+
function drainMatchingEvents(agentId, filterTypes) {
|
|
1342
|
+
const all = state.drainEvents(agentId);
|
|
1343
|
+
if (!filterTypes || filterTypes.length === 0)
|
|
1344
|
+
return all;
|
|
1345
|
+
const matching = [];
|
|
1346
|
+
const remaining = [];
|
|
1347
|
+
for (const e of all) {
|
|
1348
|
+
if (filterTypes.includes(e.type)) {
|
|
1349
|
+
matching.push(e);
|
|
1350
|
+
}
|
|
1351
|
+
else {
|
|
1352
|
+
remaining.push(e);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (remaining.length > 0)
|
|
1356
|
+
state.pushEvents(agentId, remaining);
|
|
1357
|
+
return matching;
|
|
1358
|
+
}
|
|
1359
|
+
function buildAwaitResponse(events) {
|
|
1360
|
+
return {
|
|
1361
|
+
count: events.length,
|
|
1362
|
+
events: events.map((event) => {
|
|
1363
|
+
const payload = event.payload;
|
|
1364
|
+
switch (event.type) {
|
|
1365
|
+
case "task_broadcast":
|
|
1366
|
+
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" };
|
|
1367
|
+
case "direct_message":
|
|
1368
|
+
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" };
|
|
1369
|
+
case "subtask_completed":
|
|
1370
|
+
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" };
|
|
1371
|
+
case "bid_request_confirmation":
|
|
1372
|
+
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" };
|
|
1373
|
+
case "result_submitted":
|
|
1374
|
+
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" };
|
|
1375
|
+
case "task_collected":
|
|
1376
|
+
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" };
|
|
1377
|
+
case "task_timeout":
|
|
1378
|
+
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" };
|
|
1379
|
+
default:
|
|
1380
|
+
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" };
|
|
1381
|
+
}
|
|
1382
|
+
}),
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
804
1385
|
// ---------------------------------------------------------------------------
|
|
805
1386
|
// WS Event Callbacks — auto-actions when events arrive
|
|
806
1387
|
// ---------------------------------------------------------------------------
|
|
807
1388
|
function registerEventCallbacks() {
|
|
808
1389
|
ws.setEventCallback((agentId, event) => {
|
|
809
1390
|
const taskId = event.task_id;
|
|
1391
|
+
// --- Reverse Control: try to handle event proactively ---
|
|
1392
|
+
// This runs async; if it handles the event, it may take action
|
|
1393
|
+
// (sampling, notification, auto-action) without waiting for polling.
|
|
1394
|
+
rc.handleEvent(agentId, event).catch(() => { });
|
|
1395
|
+
// --- Legacy behavior: local state updates + event buffering ---
|
|
1396
|
+
// These still run regardless of reverse control, to keep local state consistent.
|
|
810
1397
|
switch (event.type) {
|
|
811
|
-
case "
|
|
1398
|
+
case "task_collected":
|
|
812
1399
|
// Task has results ready — update local status so dashboard/skills see it
|
|
813
1400
|
state.updateTaskStatus(taskId, "awaiting_retrieval");
|
|
814
1401
|
break;
|
|
@@ -819,7 +1406,8 @@ function registerEventCallbacks() {
|
|
|
819
1406
|
net.getTaskResults(subtaskId, agentId)
|
|
820
1407
|
.then((res) => {
|
|
821
1408
|
// Buffer a synthetic event with the results for the skill to pick up
|
|
822
|
-
state.pushEvents([{
|
|
1409
|
+
state.pushEvents(agentId, [{
|
|
1410
|
+
msg_id: crypto.randomUUID().replace(/-/g, ""),
|
|
823
1411
|
type: "subtask_completed",
|
|
824
1412
|
task_id: taskId,
|
|
825
1413
|
payload: { subtask_id: subtaskId, results: res.results },
|
|
@@ -830,12 +1418,12 @@ function registerEventCallbacks() {
|
|
|
830
1418
|
}
|
|
831
1419
|
break;
|
|
832
1420
|
}
|
|
833
|
-
case "
|
|
1421
|
+
case "task_timeout":
|
|
834
1422
|
// Task timed out — auto-report reputation event, update local status
|
|
835
1423
|
state.updateTaskStatus(taskId, "no_one");
|
|
836
1424
|
net.reportEvent(agentId, "task_timeout").catch(() => { });
|
|
837
1425
|
break;
|
|
838
|
-
case "
|
|
1426
|
+
case "bid_request_confirmation":
|
|
839
1427
|
// Bid exceeded budget — mark in local state for initiator to handle
|
|
840
1428
|
// The event stays in the buffer for /eacn3-bounty to surface
|
|
841
1429
|
break;
|
|
@@ -884,7 +1472,8 @@ async function autoBidEvaluate(agentId, event) {
|
|
|
884
1472
|
}
|
|
885
1473
|
// Passed auto-filter — enrich the buffered event with a hint
|
|
886
1474
|
// The skill layer (/eacn3-bounty) will see this and can fast-track bidding
|
|
887
|
-
state.pushEvents([{
|
|
1475
|
+
state.pushEvents(agentId, [{
|
|
1476
|
+
msg_id: crypto.randomUUID().replace(/-/g, ""),
|
|
888
1477
|
type: "task_broadcast",
|
|
889
1478
|
task_id: taskId,
|
|
890
1479
|
payload: { ...payload, auto_match: true, matched_agent: agentId },
|
|
@@ -901,6 +1490,9 @@ async function main() {
|
|
|
901
1490
|
registerEventCallbacks();
|
|
902
1491
|
const transport = new StdioServerTransport();
|
|
903
1492
|
await server.connect(transport);
|
|
1493
|
+
// Initialize reverse control engine with the underlying MCP Server instance.
|
|
1494
|
+
// Must be called AFTER connect() so client capabilities are available.
|
|
1495
|
+
rc.init(server.server ?? server);
|
|
904
1496
|
}
|
|
905
1497
|
main().catch((e) => {
|
|
906
1498
|
console.error("EACN3 MCP server failed to start:", e);
|