eacn3 0.3.1 → 0.3.5

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/server.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * EACN3 MCP Server — exposes 34 tools via stdio transport.
2
+ * EACN3 MCP Server — exposes 38 tools via stdio transport.
3
3
  *
4
4
  * All intelligence lives in Skills (host LLM). This server is just
5
5
  * state management + network API wrapper. No adapter, no registry —
@@ -12,6 +12,7 @@ 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
14
  import * as ws from "./src/ws-manager.js";
15
+ import * as a2a from "./src/a2a-server.js";
15
16
  // ---------------------------------------------------------------------------
16
17
  // Helper: MCP text result
17
18
  // ---------------------------------------------------------------------------
@@ -103,7 +104,7 @@ server.tool("eacn3_cluster_status", "Retrieve the full cluster topology includin
103
104
  // Server Management (4)
104
105
  // ═══════════════════════════════════════════════════════════════════════════
105
106
  // #1 eacn3_connect
106
- 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: opens WebSocket connections for any previously registered agents. Call eacn3_register_agent next.", {
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.", {
107
108
  network_endpoint: z.string().optional().describe(`Network URL. Defaults to ${EACN3_DEFAULT_NETWORK_ENDPOINT}`),
108
109
  seed_nodes: z.array(z.string()).optional().describe("Additional seed node URLs for fallback"),
109
110
  }, async (params) => {
@@ -120,41 +121,92 @@ server.tool("eacn3_connect", "Connect to the EACN3 network — this must be your
120
121
  return err(`Cannot reach any network node: ${e.message}`);
121
122
  }
122
123
  s.network_endpoint = endpoint;
123
- // Register as server
124
- const res = await net.registerServer("0.3.0", "plugin://local", "plugin-user");
125
- s.server_card = {
126
- server_id: res.server_id,
127
- version: "0.3.0",
128
- endpoint: "plugin://local",
129
- owner: "plugin-user",
130
- status: "online",
131
- };
124
+ // Reuse existing server identity if available; otherwise register new
125
+ let sid;
126
+ if (s.server_card) {
127
+ // Try to reconnect with existing server_id via heartbeat
128
+ try {
129
+ await net.heartbeat();
130
+ sid = s.server_card.server_id;
131
+ s.server_card.status = "online";
132
+ }
133
+ catch {
134
+ // Server no longer known to network — re-register
135
+ const res = await net.registerServer("0.3.0", "plugin://local", "plugin-user");
136
+ sid = res.server_id;
137
+ s.server_card = {
138
+ server_id: sid,
139
+ version: "0.3.0",
140
+ endpoint: "plugin://local",
141
+ owner: "plugin-user",
142
+ status: "online",
143
+ };
144
+ // Update server_id on all persisted agents and re-register them with the network
145
+ for (const agent of Object.values(s.agents)) {
146
+ agent.server_id = sid;
147
+ try {
148
+ await net.registerAgent(agent);
149
+ }
150
+ catch { /* best-effort */ }
151
+ }
152
+ }
153
+ }
154
+ else {
155
+ const res = await net.registerServer("0.3.0", "plugin://local", "plugin-user");
156
+ sid = res.server_id;
157
+ s.server_card = {
158
+ server_id: sid,
159
+ version: "0.3.0",
160
+ endpoint: "plugin://local",
161
+ owner: "plugin-user",
162
+ status: "online",
163
+ };
164
+ }
132
165
  state.save();
133
166
  // Start background heartbeat
134
167
  startHeartbeat();
135
- // Reconnect WS for all existing agents
136
- for (const agentId of Object.keys(s.agents)) {
137
- ws.connect(agentId);
168
+ // Reconnect WS for all existing agents; re-register if network lost them
169
+ for (const agent of Object.values(s.agents)) {
170
+ try {
171
+ await net.getAgentInfo(agent.agent_id);
172
+ }
173
+ catch {
174
+ // Agent not found on network (e.g. server restarted with in-memory DB)
175
+ try {
176
+ await net.registerAgent(agent);
177
+ }
178
+ catch { /* best-effort */ }
179
+ }
180
+ ws.connect(agent.agent_id);
138
181
  }
182
+ const restoredAgents = Object.values(s.agents).map((a) => ({
183
+ agent_id: a.agent_id,
184
+ name: a.name,
185
+ domains: a.domains,
186
+ tier: a.tier,
187
+ }));
139
188
  return ok({
140
189
  connected: true,
141
- server_id: res.server_id,
190
+ server_id: sid,
142
191
  network_endpoint: endpoint,
143
192
  fallback,
144
- agents_online: Object.keys(s.agents).length,
193
+ agents_online: restoredAgents.length,
194
+ restored_agents: restoredAgents,
195
+ hint: restoredAgents.length > 0
196
+ ? "You have previously registered agents restored and reconnected. You can use them directly without re-registering. Call eacn3_list_my_agents() for full details."
197
+ : "No previous agents found. Register a new agent with eacn3_register_agent().",
145
198
  });
146
199
  });
147
200
  // #2 eacn3_disconnect
148
- server.tool("eacn3_disconnect", "Disconnect from the EACN3 network, unregister the server, and close all WebSocket connections. Requires: eacn3_connect first. Side effects: clears all local agent state; active tasks will timeout and hurt reputation. Returns {disconnected: true}. Only call at end of session.", {}, async () => {
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 () => {
149
202
  stopHeartbeat();
150
203
  ws.disconnectAll();
151
- try {
152
- await net.unregisterServer();
153
- }
154
- catch { /* may already be gone */ }
204
+ // Do NOT call unregisterServer — it cascade-deletes all agents on the network side.
205
+ // We only go offline; identity is preserved for reconnection.
155
206
  const s = state.getState();
156
- s.server_card = null;
157
- s.agents = {};
207
+ if (s.server_card) {
208
+ s.server_card.status = "offline";
209
+ }
158
210
  state.save();
159
211
  return ok({ disconnected: true });
160
212
  });
@@ -204,8 +256,10 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
204
256
  max_concurrent_tasks: z.number().describe("Max tasks this Agent can handle simultaneously (0 = unlimited)"),
205
257
  concurrent: z.boolean().describe("Whether this Agent supports concurrent execution"),
206
258
  }).optional().describe("Agent capacity limits"),
207
- agent_type: z.enum(["executor", "planner"]).optional().describe("Defaults to executor"),
259
+ 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."),
208
260
  agent_id: z.string().optional().describe("Custom agent ID. Auto-generated if omitted."),
261
+ a2a_port: z.number().optional().describe("Port for A2A HTTP server. Enables direct agent-to-agent messaging. Omit to use Network relay only."),
262
+ 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."),
209
263
  }, async (params) => {
210
264
  const s = state.getState();
211
265
  if (!s.server_card)
@@ -217,15 +271,27 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
217
271
  return err("domains cannot be empty");
218
272
  const agentId = params.agent_id ?? `agent-${Date.now().toString(36)}`;
219
273
  const sid = s.server_card.server_id;
274
+ // Determine agent URL: real A2A endpoint or local placeholder
275
+ let agentUrl = `plugin://local/agents/${agentId}`;
276
+ if (params.a2a_port || params.a2a_url) {
277
+ const port = params.a2a_port ?? 0;
278
+ const actualPort = await a2a.startServer(port);
279
+ if (params.a2a_url) {
280
+ agentUrl = `${params.a2a_url.replace(/\/$/, "")}/agents/${agentId}`;
281
+ }
282
+ else {
283
+ agentUrl = `http://localhost:${actualPort}/agents/${agentId}`;
284
+ }
285
+ }
220
286
  // Assemble AgentCard (what adapter used to do)
221
287
  const card = {
222
288
  agent_id: agentId,
223
289
  name: params.name,
224
- agent_type: params.agent_type ?? "executor",
290
+ tier: params.tier ?? "general",
225
291
  domains: params.domains,
226
292
  skills: params.skills ?? [],
227
293
  capabilities: params.capabilities,
228
- url: `plugin://local/agents/${agentId}`,
294
+ url: agentUrl,
229
295
  server_id: sid,
230
296
  network_id: "",
231
297
  description: params.description,
@@ -241,10 +307,12 @@ server.tool("eacn3_register_agent", "Create and register an agent identity on th
241
307
  agent_id: agentId,
242
308
  seeds: res.seeds,
243
309
  domains: params.domains,
310
+ url: agentUrl,
311
+ a2a_server: a2a.isRunning() ? { port: a2a.getServerPort() } : null,
244
312
  });
245
313
  });
246
314
  // #6 eacn3_get_agent
247
- 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, agent_type, domains, skills, capabilities, url, server_id, description}. No side effects. Use to inspect an agent before sending messages or evaluating bids.", {
315
+ 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.", {
248
316
  agent_id: z.string(),
249
317
  }, async (params) => {
250
318
  // Check local first
@@ -293,17 +361,20 @@ server.tool("eacn3_unregister_agent", "Remove an agent from the network and clos
293
361
  const res = await net.unregisterAgent(params.agent_id);
294
362
  ws.disconnect(params.agent_id);
295
363
  state.removeAgent(params.agent_id);
364
+ // Stop A2A server if no agents remain
365
+ if (state.listAgents().length === 0 && a2a.isRunning()) {
366
+ await a2a.stopServer();
367
+ }
296
368
  return ok({ unregistered: true, agent_id: params.agent_id, ...res });
297
369
  });
298
370
  // #9 eacn3_list_my_agents
299
- 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, agent_type, domains, and ws_connected (WebSocket status). No network call — reads local state only. Use to check which agents are active and receiving events.", {}, async () => {
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 () => {
300
372
  const agents = state.listAgents();
301
373
  return ok({
302
374
  count: agents.length,
303
375
  agents: agents.map((a) => ({
304
376
  agent_id: a.agent_id,
305
377
  name: a.name,
306
- agent_type: a.agent_type,
307
378
  domains: a.domains,
308
379
  ws_connected: ws.isConnected(a.agent_id),
309
380
  })),
@@ -386,6 +457,8 @@ server.tool("eacn3_create_task", "Publish a new task to the EACN3 network for ot
386
457
  contact_id: z.string().optional().describe("Human contact identifier"),
387
458
  timeout_s: z.number().optional().describe("Seconds to wait for human response before auto-reject"),
388
459
  }).optional().describe("Human-in-the-loop contact settings"),
460
+ level: z.enum(["general", "expert", "expert_general", "tool"]).optional().describe("Task complexity level. Determines which agent tiers can bid. 'tool' = only tool-level tasks for simple tool wrappers. Defaults to 'general' (open to all)."),
461
+ invited_agent_ids: z.array(z.string()).optional().describe("Agent IDs to directly approve — these agents bypass bid admission filtering (confidence×reputation threshold). Use to pre-select specific agents you trust."),
389
462
  initiator_id: z.string().optional().describe("Agent ID of the task initiator (auto-injected if omitted)"),
390
463
  }, async (params) => {
391
464
  const initiatorId = resolveAgentId(params.initiator_id);
@@ -409,6 +482,8 @@ server.tool("eacn3_create_task", "Publish a new task to the EACN3 network for ot
409
482
  max_concurrent_bidders: params.max_concurrent_bidders,
410
483
  max_depth: params.max_depth,
411
484
  human_contact: params.human_contact,
485
+ level: params.level ?? "general",
486
+ invited_agent_ids: params.invited_agent_ids,
412
487
  });
413
488
  // Track locally
414
489
  state.updateTask({
@@ -485,17 +560,79 @@ server.tool("eacn3_confirm_budget", "Approve or reject a bid that exceeded your
485
560
  const res = await net.confirmBudget(params.task_id, initiatorId, params.approved, params.new_budget);
486
561
  return ok(res);
487
562
  });
563
+ // #22b eacn3_invite_agent
564
+ server.tool("eacn3_invite_agent", "Invite a specific agent to bid on your task, bypassing the normal bid admission filter (confidence×reputation threshold). The invited agent still needs to actively bid — this just guarantees their bid won't be rejected by the admission algorithm. Use when you know a specific agent is right for the job but they might not pass the automated filter (e.g. new agent with low reputation). Also sends a direct_message notification to the invited agent. Requires: you must be the task initiator.", {
565
+ task_id: z.string(),
566
+ agent_id: z.string().describe("Agent ID to invite"),
567
+ message: z.string().optional().describe("Optional message to send with the invitation"),
568
+ initiator_id: z.string().optional().describe("Initiator agent ID (auto-injected if omitted)"),
569
+ }, async (params) => {
570
+ const initiatorId = resolveAgentId(params.initiator_id);
571
+ const res = await net.inviteAgent(params.task_id, initiatorId, params.agent_id);
572
+ // Send a direct_message notification to the invited agent
573
+ const inviteContent = params.message
574
+ ? `[Task Invitation] You've been invited to bid on task ${params.task_id}. Your bid will bypass admission filtering. Message from initiator: ${params.message}`
575
+ : `[Task Invitation] You've been invited to bid on task ${params.task_id}. Your bid will bypass admission filtering.`;
576
+ // Record outgoing message in session
577
+ state.addMessage(initiatorId, {
578
+ from: initiatorId,
579
+ to: params.agent_id,
580
+ content: inviteContent,
581
+ timestamp: Date.now(),
582
+ direction: "out",
583
+ });
584
+ // Try to notify the invited agent
585
+ try {
586
+ const agentCard = await net.getAgentInfo(params.agent_id);
587
+ if (agentCard.url && !agentCard.url.startsWith("plugin://")) {
588
+ const eventsUrl = agentCard.url.replace(/\/$/, "") + "/events";
589
+ await fetch(eventsUrl, {
590
+ method: "POST",
591
+ headers: { "Content-Type": "application/json" },
592
+ body: JSON.stringify({
593
+ type: "direct_message",
594
+ from: initiatorId,
595
+ content: inviteContent,
596
+ task_id: params.task_id,
597
+ invitation: true,
598
+ }),
599
+ }).catch(() => { });
600
+ }
601
+ else {
602
+ // Relay via network
603
+ await net.relayMessage({
604
+ to: {
605
+ network_id: agentCard.network_id ?? "",
606
+ server_id: agentCard.server_id,
607
+ agent_id: params.agent_id,
608
+ },
609
+ from: {
610
+ network_id: state.getState().server_card?.server_id ?? "",
611
+ server_id: state.getServerId() ?? "",
612
+ agent_id: initiatorId,
613
+ },
614
+ content: inviteContent,
615
+ }).catch(() => { });
616
+ }
617
+ }
618
+ catch {
619
+ // Agent lookup failed — invitation still recorded server-side
620
+ }
621
+ return ok(res);
622
+ });
488
623
  // ═══════════════════════════════════════════════════════════════════════════
489
624
  // Task Operations — Executor (5)
490
625
  // ═══════════════════════════════════════════════════════════════════════════
491
626
  // #23 eacn3_submit_bid
492
- server.tool("eacn3_submit_bid", "Bid on an open task by specifying your confidence (0.0-1.0 honest ability estimate) and price in credits. Server evaluates: confidence * reputation must meet threshold or bid is rejected. Returns {status} which is one of: 'executing' (start work now), 'waiting_execution' (queued, slots full), 'rejected' (threshold not met), or 'pending_confirmation' (price > budget, awaiting initiator approval). Side effects: if accepted, tracks task locally as executor role. If price > budget, initiator gets a 'budget_confirmation' event.", {
627
+ server.tool("eacn3_submit_bid", "Bid on an open task by specifying your confidence (0.0-1.0 honest ability estimate) and price in credits. Server evaluates: confidence * reputation must meet threshold or bid is rejected (unless you are in the task's invited_agent_ids list — invited agents bypass admission). Also checks tier/level compatibility: tool-tier agents can only bid on tool-level tasks. Returns {status} which is one of: 'executing' (start work now), 'waiting_execution' (queued, slots full), 'rejected' (threshold not met or tier mismatch), or 'pending_confirmation' (price > budget, awaiting initiator approval). Side effects: if accepted, tracks task locally as executor role.", {
493
628
  task_id: z.string(),
494
629
  confidence: z.number().min(0).max(1).describe("0.0-1.0 confidence in ability to complete"),
495
630
  price: z.number().describe("Bid price"),
496
631
  agent_id: z.string().optional().describe("Bidder agent ID (auto-injected if omitted)"),
497
632
  }, async (params) => {
498
633
  const agentId = resolveAgentId(params.agent_id);
634
+ // Tier/level filtering and invite bypass are handled server-side in matcher.check_bid().
635
+ // No client-side pre-flight — the network returns "rejected" with reason for tier mismatches.
499
636
  const res = await net.submitBid(params.task_id, agentId, params.confidence, params.price);
500
637
  // Track locally if not rejected (status could be "executing", "waiting_execution", etc.)
501
638
  if (res.status && res.status !== "rejected") {
@@ -549,10 +686,11 @@ server.tool("eacn3_create_subtask", "Delegate part of your work by creating a ch
549
686
  domains: z.array(z.string()),
550
687
  budget: z.number(),
551
688
  deadline: z.string().optional(),
689
+ level: z.enum(["general", "expert", "expert_general", "tool"]).optional().describe("Task level for the subtask. If omitted, inherits from parent task."),
552
690
  initiator_id: z.string().optional().describe("Agent ID of the executor creating the subtask (auto-injected if omitted)"),
553
691
  }, async (params) => {
554
692
  const initiatorId = resolveAgentId(params.initiator_id);
555
- const task = await net.createSubtask(params.parent_task_id, initiatorId, { description: params.description }, params.domains, params.budget, params.deadline);
693
+ const task = await net.createSubtask(params.parent_task_id, initiatorId, { description: params.description }, params.domains, params.budget, params.deadline, params.level);
556
694
  return ok({
557
695
  subtask_id: task.id,
558
696
  parent_task_id: params.parent_task_id,
@@ -561,27 +699,34 @@ server.tool("eacn3_create_subtask", "Delegate part of your work by creating a ch
561
699
  });
562
700
  });
563
701
  // #27 eacn3_send_message
564
- // A2A direct — agent.md:358-362: peer-to-peer, bypasses Network
565
- server.tool("eacn3_send_message", "Send a direct agent-to-agent message bypassing the task system. Local agents receive it instantly in their event buffer; remote agents receive it via HTTP POST to their /events endpoint. Returns {sent, to, from, local}. The recipient sees a 'direct_message' event with payload.from and payload.content. Will fail if the remote agent has no reachable URL or is offline.", {
702
+ // 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.", {
566
704
  agent_id: z.string().describe("Target agent ID"),
567
705
  content: z.string(),
568
706
  sender_id: z.string().optional().describe("Your agent ID (auto-injected if omitted)"),
569
707
  }, async (params) => {
570
708
  const senderId = params.sender_id ?? resolveAgentId();
571
709
  const targetId = params.agent_id;
572
- const message = {
573
- type: "direct_message",
574
- task_id: "",
575
- payload: { from: senderId, content: params.content },
576
- received_at: Date.now(),
577
- };
578
- // Local agent — direct push to event buffer
710
+ // Record outgoing message in session
711
+ state.addMessage(senderId, {
712
+ from: senderId,
713
+ to: targetId,
714
+ content: params.content,
715
+ timestamp: Date.now(),
716
+ direction: "out",
717
+ });
718
+ // 1. Local agent — direct push to event buffer
579
719
  const localAgent = state.getAgent(targetId);
580
720
  if (localAgent) {
581
- state.pushEvents([message]);
582
- return ok({ sent: true, to: targetId, from: senderId, local: true });
721
+ state.pushEvents([{
722
+ type: "direct_message",
723
+ task_id: "",
724
+ payload: { from: senderId, content: params.content },
725
+ received_at: Date.now(),
726
+ }]);
727
+ return ok({ sent: true, to: targetId, from: senderId, method: "local" });
583
728
  }
584
- // Remote agent — POST to their URL callback (A2A direct, agent.md:160-168)
729
+ // 2. Remote agent — look up AgentCard
585
730
  let agentCard;
586
731
  try {
587
732
  agentCard = await net.getAgentInfo(targetId);
@@ -589,28 +734,47 @@ server.tool("eacn3_send_message", "Send a direct agent-to-agent message bypassin
589
734
  catch {
590
735
  return err(`Agent ${targetId} not found`);
591
736
  }
592
- if (!agentCard.url || agentCard.url.startsWith("plugin://")) {
593
- return err(`Agent ${targetId} has no reachable URL: ${agentCard.url}`);
737
+ // 3. Try A2A direct if agent has a real HTTP URL
738
+ if (agentCard.url && !agentCard.url.startsWith("plugin://")) {
739
+ const eventsUrl = agentCard.url.replace(/\/$/, "") + "/events";
740
+ try {
741
+ const res = await fetch(eventsUrl, {
742
+ method: "POST",
743
+ headers: { "Content-Type": "application/json" },
744
+ body: JSON.stringify({
745
+ type: "direct_message",
746
+ from: senderId,
747
+ content: params.content,
748
+ }),
749
+ });
750
+ if (res.ok) {
751
+ return ok({ sent: true, to: targetId, from: senderId, method: "a2a_direct" });
752
+ }
753
+ // Direct failed — fall through to relay
754
+ }
755
+ catch {
756
+ // Direct failed — fall through to relay
757
+ }
594
758
  }
595
- // POST /events on agent's URL (agent.md:164)
596
- const eventsUrl = agentCard.url.replace(/\/$/, "") + "/events";
759
+ // 4. Network relay fallback route via Network node using three-layer addressing
597
760
  try {
598
- const res = await fetch(eventsUrl, {
599
- method: "POST",
600
- headers: { "Content-Type": "application/json" },
601
- body: JSON.stringify({
602
- type: "direct_message",
603
- from: senderId,
604
- content: params.content,
605
- }),
761
+ await net.relayMessage({
762
+ to: {
763
+ network_id: agentCard.network_id ?? "",
764
+ server_id: agentCard.server_id,
765
+ agent_id: targetId,
766
+ },
767
+ from: {
768
+ network_id: state.getState().server_card?.server_id ?? "",
769
+ server_id: state.getServerId() ?? "",
770
+ agent_id: senderId,
771
+ },
772
+ content: params.content,
606
773
  });
607
- if (!res.ok) {
608
- return err(`POST ${eventsUrl} failed: ${res.status}`);
609
- }
610
- return ok({ sent: true, to: targetId, from: senderId, local: false });
774
+ return ok({ sent: true, to: targetId, from: senderId, method: "relay" });
611
775
  }
612
776
  catch (e) {
613
- return err(`Failed to reach agent at ${eventsUrl}: ${e.message}`);
777
+ return err(`All delivery methods failed for ${targetId}: ${e.message}`);
614
778
  }
615
779
  });
616
780
  // ═══════════════════════════════════════════════════════════════════════════
@@ -654,7 +818,27 @@ server.tool("eacn3_deposit", "Add EACN credits to an agent's available balance.
654
818
  // ═══════════════════════════════════════════════════════════════════════════
655
819
  // Events (1)
656
820
  // ═══════════════════════════════════════════════════════════════════════════
657
- // #32 eacn3_get_events
821
+ // Messaging (2)
822
+ // ═══════════════════════════════════════════════════════════════════════════
823
+ // #32 eacn3_get_messages
824
+ server.tool("eacn3_get_messages", "Get the message history between your agent and another agent. Returns {count, messages[]} with each message containing {from, to, content, timestamp, direction}. direction is 'in' (received) or 'out' (sent). Messages are stored per-session, capped at 100 per peer. Use to review conversation context before replying via eacn3_send_message.", {
825
+ agent_id: z.string().optional().describe("Your agent ID (auto-injected if only one registered)"),
826
+ peer_agent_id: z.string().describe("The other agent's ID"),
827
+ }, async (params) => {
828
+ const agentId = params.agent_id ?? resolveAgentId();
829
+ const messages = state.getMessages(agentId, params.peer_agent_id);
830
+ return ok({ count: messages.length, messages });
831
+ });
832
+ // #33 eacn3_list_sessions
833
+ server.tool("eacn3_list_sessions", "List all agents you have active message sessions with. Returns {count, peers[]} where each peer is an agent_id. Use to discover ongoing conversations. Check individual sessions with eacn3_get_messages.", {
834
+ agent_id: z.string().optional().describe("Your agent ID (auto-injected if only one registered)"),
835
+ }, async (params) => {
836
+ const agentId = params.agent_id ?? resolveAgentId();
837
+ const peers = state.listSessions(agentId);
838
+ return ok({ count: peers.length, peers });
839
+ });
840
+ // ═══════════════════════════════════════════════════════════════════════════
841
+ // #34 eacn3_get_events
658
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 () => {
659
843
  const events = state.drainEvents();
660
844
  return ok({
@@ -707,6 +891,22 @@ function registerEventCallbacks() {
707
891
  // New task available — auto-evaluate bid if agent has matching domains
708
892
  autoBidEvaluate(agentId, event).catch(() => { });
709
893
  break;
894
+ case "direct_message": {
895
+ // Another agent sent a direct message — store in session
896
+ const payload = event.payload;
897
+ const from = payload?.from;
898
+ const content = payload?.content;
899
+ if (from && content !== undefined) {
900
+ state.addMessage(agentId, {
901
+ from,
902
+ to: agentId,
903
+ content: typeof content === "string" ? content : JSON.stringify(content),
904
+ timestamp: Date.now(),
905
+ direction: "in",
906
+ });
907
+ }
908
+ break;
909
+ }
710
910
  }
711
911
  });
712
912
  }