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/index.d.ts +8 -2
- package/dist/index.js +724 -573
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.js +261 -61
- package/dist/server.js.map +1 -1
- package/dist/src/a2a-server.d.ts +27 -0
- package/dist/src/a2a-server.js +146 -0
- package/dist/src/a2a-server.js.map +1 -0
- package/dist/src/models.d.ts +83 -2
- package/dist/src/models.js +23 -0
- package/dist/src/models.js.map +1 -1
- package/dist/src/network-client.d.ts +26 -2
- package/dist/src/network-client.js +16 -1
- package/dist/src/network-client.js.map +1 -1
- package/dist/src/state.d.ts +15 -1
- package/dist/src/state.js +75 -5
- package/dist/src/state.js.map +1 -1
- package/package.json +1 -1
- package/scripts/cli.cjs +11 -10
- package/skills/eacn3-bid/SKILL.md +13 -3
- package/skills/eacn3-browse/SKILL.md +1 -1
- package/skills/eacn3-invite/SKILL.md +90 -0
- package/skills/eacn3-invite-zh/SKILL.md +90 -0
- package/skills/eacn3-message/SKILL.md +67 -0
- package/skills/eacn3-message-zh/SKILL.md +67 -0
- package/skills/eacn3-register/SKILL.md +17 -13
- package/skills/eacn3-register-zh/SKILL.md +3 -15
- package/skills/eacn3-task/SKILL.md +4 -0
- package/skills/eacn3-task-zh/SKILL.md +4 -0
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EACN3 MCP Server — exposes
|
|
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.
|
|
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
|
-
//
|
|
124
|
-
|
|
125
|
-
s.server_card
|
|
126
|
-
server_id
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
137
|
-
|
|
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:
|
|
190
|
+
server_id: sid,
|
|
142
191
|
network_endpoint: endpoint,
|
|
143
192
|
fallback,
|
|
144
|
-
agents_online:
|
|
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
|
|
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
|
-
|
|
152
|
-
|
|
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
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
+
tier: params.tier ?? "general",
|
|
225
291
|
domains: params.domains,
|
|
226
292
|
skills: params.skills ?? [],
|
|
227
293
|
capabilities: params.capabilities,
|
|
228
|
-
url:
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
|
565
|
-
server.tool("eacn3_send_message", "Send a direct agent-to-agent message
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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([
|
|
582
|
-
|
|
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 —
|
|
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
|
-
|
|
593
|
-
|
|
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
|
-
//
|
|
596
|
-
const eventsUrl = agentCard.url.replace(/\/$/, "") + "/events";
|
|
759
|
+
// 4. Network relay fallback — route via Network node using three-layer addressing
|
|
597
760
|
try {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
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(`
|
|
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
|
-
//
|
|
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
|
}
|