eacn3 0.3.5 → 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.
Files changed (44) hide show
  1. package/dist/index.js +224 -26
  2. package/dist/index.js.map +1 -1
  3. package/dist/server.js +571 -27
  4. package/dist/server.js.map +1 -1
  5. package/dist/src/a2a-server.js +2 -1
  6. package/dist/src/a2a-server.js.map +1 -1
  7. package/dist/src/event-transport.d.ts +31 -0
  8. package/dist/src/event-transport.js +178 -0
  9. package/dist/src/event-transport.js.map +1 -0
  10. package/dist/src/models.d.ts +55 -11
  11. package/dist/src/models.js +1 -1
  12. package/dist/src/models.js.map +1 -1
  13. package/dist/src/network-client.d.ts +1 -1
  14. package/dist/src/network-client.js +4 -5
  15. package/dist/src/network-client.js.map +1 -1
  16. package/dist/src/reverse-control.d.ts +74 -0
  17. package/dist/src/reverse-control.js +609 -0
  18. package/dist/src/reverse-control.js.map +1 -0
  19. package/dist/src/state.d.ts +17 -4
  20. package/dist/src/state.js +115 -14
  21. package/dist/src/state.js.map +1 -1
  22. package/openclaw.plugin.json +1 -1
  23. package/package.json +2 -6
  24. package/scripts/cli.cjs +3 -3
  25. package/skills/eacn3-bid/SKILL.md +2 -2
  26. package/skills/eacn3-bid-zh/SKILL.md +2 -2
  27. package/skills/eacn3-bounty/SKILL.md +7 -7
  28. package/skills/eacn3-bounty-zh/SKILL.md +7 -7
  29. package/skills/eacn3-budget/SKILL.md +1 -1
  30. package/skills/eacn3-budget-zh/SKILL.md +1 -1
  31. package/skills/eacn3-clarify/SKILL.md +1 -1
  32. package/skills/eacn3-clarify-zh/SKILL.md +1 -1
  33. package/skills/eacn3-collect/SKILL.md +1 -1
  34. package/skills/eacn3-collect-zh/SKILL.md +1 -1
  35. package/skills/eacn3-dashboard/SKILL.md +3 -3
  36. package/skills/eacn3-dashboard-zh/SKILL.md +3 -3
  37. package/skills/eacn3-delegate/SKILL.md +1 -1
  38. package/skills/eacn3-delegate-zh/SKILL.md +1 -1
  39. package/skills/eacn3-execute/SKILL.md +1 -1
  40. package/skills/eacn3-execute-zh/SKILL.md +1 -1
  41. package/skills/eacn3-invite/SKILL.md +1 -1
  42. package/skills/eacn3-invite-zh/SKILL.md +1 -1
  43. package/skills/eacn3-task/SKILL.md +1 -1
  44. 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/ws-manager.js";
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
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
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.3.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, restored_agents, hint}. Side effects: opens WebSocket connections 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.", {
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) => {
@@ -132,11 +143,11 @@ server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your
132
143
  }
133
144
  catch {
134
145
  // Server no longer known to network — re-register
135
- const res = await net.registerServer("0.3.0", "plugin://local", "plugin-user");
146
+ const res = await net.registerServer("0.5.0", "plugin://local", "plugin-user");
136
147
  sid = res.server_id;
137
148
  s.server_card = {
138
149
  server_id: sid,
139
- version: "0.3.0",
150
+ version: "0.5.0",
140
151
  endpoint: "plugin://local",
141
152
  owner: "plugin-user",
142
153
  status: "online",
@@ -152,11 +163,11 @@ server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your
152
163
  }
153
164
  }
154
165
  else {
155
- const res = await net.registerServer("0.3.0", "plugin://local", "plugin-user");
166
+ const res = await net.registerServer("0.5.0", "plugin://local", "plugin-user");
156
167
  sid = res.server_id;
157
168
  s.server_card = {
158
169
  server_id: sid,
159
- version: "0.3.0",
170
+ version: "0.5.0",
160
171
  endpoint: "plugin://local",
161
172
  owner: "plugin-user",
162
173
  status: "online",
@@ -195,10 +206,28 @@ server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your
195
206
  hint: restoredAgents.length > 0
196
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."
197
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
+ },
198
227
  });
199
228
  });
200
229
  // #2 eacn3_disconnect
201
- server.tool("eacn3_disconnect", "Disconnect from the EACN3 network and close all WebSocket connections. 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 () => {
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 () => {
202
231
  stopHeartbeat();
203
232
  ws.disconnectAll();
204
233
  // Do NOT call unregisterServer — it cascade-deletes all agents on the network side.
@@ -241,7 +270,7 @@ server.tool("eacn3_server_info", "Get current server connection state, including
241
270
  // ═══════════════════════════════════════════════════════════════════════════
242
271
  // #5 eacn3_register_agent
243
272
  // 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 opens a WebSocket for real-time event push (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').", {
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').", {
245
274
  name: z.string().describe("Agent display name"),
246
275
  description: z.string().describe("What this Agent does"),
247
276
  domains: z.array(z.string()).describe("Capability domains (e.g. ['translation', 'coding'])"),
@@ -260,6 +289,11 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
260
289
  agent_id: z.string().optional().describe("Custom agent ID. Auto-generated if omitted."),
261
290
  a2a_port: z.number().optional().describe("Port for A2A HTTP server. Enables direct agent-to-agent messaging. Omit to use Network relay only."),
262
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."),
263
297
  }, async (params) => {
264
298
  const s = state.getState();
265
299
  if (!s.server_card)
@@ -300,8 +334,21 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
300
334
  const res = await net.registerAgent(card);
301
335
  // Persist locally
302
336
  state.addAgent(card);
303
- // Open WebSocket for event push
337
+ // Start polling for push events
304
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();
305
352
  return ok({
306
353
  registered: true,
307
354
  agent_id: agentId,
@@ -309,6 +356,29 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
309
356
  domains: params.domains,
310
357
  url: agentUrl,
311
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
+ },
312
382
  });
313
383
  });
314
384
  // #6 eacn3_get_agent
@@ -355,11 +425,12 @@ server.tool("eacn3_update_agent", "Update a registered agent's mutable fields: n
355
425
  return ok({ updated: true, agent_id, ...res });
356
426
  });
357
427
  // #8 eacn3_unregister_agent
358
- server.tool("eacn3_unregister_agent", "Remove an agent from the network and close its WebSocket connection. 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}.", {
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}.", {
359
429
  agent_id: z.string(),
360
430
  }, async (params) => {
361
431
  const res = await net.unregisterAgent(params.agent_id);
362
432
  ws.disconnect(params.agent_id);
433
+ rc.unconfigure(params.agent_id);
363
434
  state.removeAgent(params.agent_id);
364
435
  // Stop A2A server if no agents remain
365
436
  if (state.listAgents().length === 0 && a2a.isRunning()) {
@@ -368,7 +439,7 @@ server.tool("eacn3_unregister_agent", "Remove an agent from the network and clos
368
439
  return ok({ unregistered: true, agent_id: params.agent_id, ...res });
369
440
  });
370
441
  // #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 ws_connected (WebSocket status). No network call — reads local state only. Use to check which agents are active and receiving events.", {}, async () => {
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 () => {
372
443
  const agents = state.listAgents();
373
444
  return ok({
374
445
  count: agents.length,
@@ -376,7 +447,8 @@ server.tool("eacn3_list_my_agents", "List all agents registered on this local se
376
447
  agent_id: a.agent_id,
377
448
  name: a.name,
378
449
  domains: a.domains,
379
- ws_connected: ws.isConnected(a.agent_id),
450
+ connected: ws.isConnected(a.agent_id),
451
+ transport: ws.getTransportStatus(a.agent_id),
380
452
  })),
381
453
  });
382
454
  });
@@ -444,6 +516,7 @@ server.tool("eacn3_list_tasks", "Browse all tasks on the network with optional f
444
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.", {
445
517
  description: z.string(),
446
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."),
447
520
  domains: z.array(z.string()).optional(),
448
521
  deadline: z.string().optional().describe("ISO 8601 deadline"),
449
522
  max_concurrent_bidders: z.number().optional(),
@@ -469,11 +542,48 @@ server.tool("eacn3_create_task", "Publish a new task to the EACN3 network for ot
469
542
  ? localAgents.filter((a) => a.agent_id !== initiatorId &&
470
543
  params.domains.some((d) => a.domains.includes(d)))
471
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
+ }
472
582
  const task = await net.createTask({
473
583
  task_id: taskId,
474
584
  initiator_id: initiatorId,
475
585
  content: {
476
- description: params.description,
586
+ description: finalDescription,
477
587
  expected_output: params.expected_output,
478
588
  },
479
589
  domains: params.domains,
@@ -488,6 +598,7 @@ server.tool("eacn3_create_task", "Publish a new task to the EACN3 network for ot
488
598
  // Track locally
489
599
  state.updateTask({
490
600
  task_id: taskId,
601
+ agent_id: initiatorId,
491
602
  role: "initiator",
492
603
  status: task.status,
493
604
  domains: params.domains ?? [],
@@ -518,6 +629,25 @@ server.tool("eacn3_select_result", "Pick the winning result for a task, triggeri
518
629
  }, async (params) => {
519
630
  const initiatorId = resolveAgentId(params.initiator_id);
520
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
+ }
521
651
  return ok(res);
522
652
  });
523
653
  // #19 eacn3_close_task
@@ -540,7 +670,7 @@ server.tool("eacn3_update_deadline", "Extend or shorten a task's deadline. Requi
540
670
  return ok(res);
541
671
  });
542
672
  // #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 'discussions_updated' WebSocket event to all bidding agents. Returns confirmation. Use to provide additional context or answer bidder questions.", {
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.", {
544
674
  task_id: z.string(),
545
675
  message: z.string(),
546
676
  initiator_id: z.string().optional().describe("Initiator agent ID (auto-injected if omitted)"),
@@ -550,7 +680,7 @@ server.tool("eacn3_update_discussions", "Post a clarification or discussion mess
550
680
  return ok(res);
551
681
  });
552
682
  // #22 eacn3_confirm_budget
553
- server.tool("eacn3_confirm_budget", "Approve or reject a bid that exceeded your task's budget, triggered by a 'budget_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.", {
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.", {
554
684
  task_id: z.string(),
555
685
  approved: z.boolean(),
556
686
  new_budget: z.number().optional(),
@@ -638,6 +768,7 @@ server.tool("eacn3_submit_bid", "Bid on an open task by specifying your confiden
638
768
  if (res.status && res.status !== "rejected") {
639
769
  state.updateTask({
640
770
  task_id: params.task_id,
771
+ agent_id: agentId,
641
772
  role: "executor",
642
773
  status: "bidding",
643
774
  domains: [],
@@ -700,7 +831,7 @@ server.tool("eacn3_create_subtask", "Delegate part of your work by creating a ch
700
831
  });
701
832
  // #27 eacn3_send_message
702
833
  // 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 via WebSocket. 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.", {
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.", {
704
835
  agent_id: z.string().describe("Target agent ID"),
705
836
  content: z.string(),
706
837
  sender_id: z.string().optional().describe("Your agent ID (auto-injected if omitted)"),
@@ -718,7 +849,8 @@ server.tool("eacn3_send_message", "Send a direct agent-to-agent message. Deliver
718
849
  // 1. Local agent — direct push to event buffer
719
850
  const localAgent = state.getAgent(targetId);
720
851
  if (localAgent) {
721
- state.pushEvents([{
852
+ state.pushEvents(targetId, [{
853
+ msg_id: crypto.randomUUID().replace(/-/g, ""),
722
854
  type: "direct_message",
723
855
  task_id: "",
724
856
  payload: { from: senderId, content: params.content },
@@ -839,24 +971,431 @@ server.tool("eacn3_list_sessions", "List all agents you have active message sess
839
971
  });
840
972
  // ═══════════════════════════════════════════════════════════════════════════
841
973
  // #34 eacn3_get_events
842
- server.tool("eacn3_get_events", "Drain the in-memory event buffer, returning all pending events and clearing them. Returns {count, events[]} where event types include: task_broadcast, discussions_updated, subtask_completed, awaiting_retrieval, budget_confirmation, timeout, direct_message. Call periodically in your main loop. Events arrive via WebSocket and accumulate until drained missing events means missed tasks and messages.", {}, async () => {
843
- const events = state.drainEvents();
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);
844
979
  return ok({
845
980
  count: events.length,
846
981
  events,
982
+ reverse_control: rc.getStatus(),
983
+ });
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",
847
1321
  });
848
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
+ });
849
1338
  // ---------------------------------------------------------------------------
850
- // Start
1339
+ // Long-polling helpers
851
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
+ }
852
1385
  // ---------------------------------------------------------------------------
853
1386
  // WS Event Callbacks — auto-actions when events arrive
854
1387
  // ---------------------------------------------------------------------------
855
1388
  function registerEventCallbacks() {
856
1389
  ws.setEventCallback((agentId, event) => {
857
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.
858
1397
  switch (event.type) {
859
- case "awaiting_retrieval":
1398
+ case "task_collected":
860
1399
  // Task has results ready — update local status so dashboard/skills see it
861
1400
  state.updateTaskStatus(taskId, "awaiting_retrieval");
862
1401
  break;
@@ -867,7 +1406,8 @@ function registerEventCallbacks() {
867
1406
  net.getTaskResults(subtaskId, agentId)
868
1407
  .then((res) => {
869
1408
  // Buffer a synthetic event with the results for the skill to pick up
870
- state.pushEvents([{
1409
+ state.pushEvents(agentId, [{
1410
+ msg_id: crypto.randomUUID().replace(/-/g, ""),
871
1411
  type: "subtask_completed",
872
1412
  task_id: taskId,
873
1413
  payload: { subtask_id: subtaskId, results: res.results },
@@ -878,12 +1418,12 @@ function registerEventCallbacks() {
878
1418
  }
879
1419
  break;
880
1420
  }
881
- case "timeout":
1421
+ case "task_timeout":
882
1422
  // Task timed out — auto-report reputation event, update local status
883
1423
  state.updateTaskStatus(taskId, "no_one");
884
1424
  net.reportEvent(agentId, "task_timeout").catch(() => { });
885
1425
  break;
886
- case "budget_confirmation":
1426
+ case "bid_request_confirmation":
887
1427
  // Bid exceeded budget — mark in local state for initiator to handle
888
1428
  // The event stays in the buffer for /eacn3-bounty to surface
889
1429
  break;
@@ -932,7 +1472,8 @@ async function autoBidEvaluate(agentId, event) {
932
1472
  }
933
1473
  // Passed auto-filter — enrich the buffered event with a hint
934
1474
  // The skill layer (/eacn3-bounty) will see this and can fast-track bidding
935
- state.pushEvents([{
1475
+ state.pushEvents(agentId, [{
1476
+ msg_id: crypto.randomUUID().replace(/-/g, ""),
936
1477
  type: "task_broadcast",
937
1478
  task_id: taskId,
938
1479
  payload: { ...payload, auto_match: true, matched_agent: agentId },
@@ -949,6 +1490,9 @@ async function main() {
949
1490
  registerEventCallbacks();
950
1491
  const transport = new StdioServerTransport();
951
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);
952
1496
  }
953
1497
  main().catch((e) => {
954
1498
  console.error("EACN3 MCP server failed to start:", e);