eacn3 0.3.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_GUIDE.md +345 -0
- package/dist/index.js +259 -48
- package/dist/index.js.map +1 -1
- package/dist/server.js +978 -75
- package/dist/server.js.map +1 -1
- package/dist/src/a2a-server.js +2 -1
- package/dist/src/a2a-server.js.map +1 -1
- package/dist/src/event-transport.d.ts +48 -0
- package/dist/src/event-transport.js +156 -0
- package/dist/src/event-transport.js.map +1 -0
- package/dist/src/models.d.ts +59 -11
- package/dist/src/models.js +1 -1
- package/dist/src/models.js.map +1 -1
- package/dist/src/network-client.d.ts +3 -1
- package/dist/src/network-client.js +87 -14
- package/dist/src/network-client.js.map +1 -1
- package/dist/src/reverse-control.d.ts +74 -0
- package/dist/src/reverse-control.js +609 -0
- package/dist/src/reverse-control.js.map +1 -0
- package/dist/src/state.d.ts +50 -4
- package/dist/src/state.js +492 -43
- package/dist/src/state.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -7
- package/scripts/cli.cjs +28 -11
- package/skills/eacn3-bid/SKILL.md +2 -2
- package/skills/eacn3-bid-zh/SKILL.md +2 -2
- package/skills/eacn3-bounty/SKILL.md +7 -7
- package/skills/eacn3-bounty-zh/SKILL.md +7 -7
- package/skills/eacn3-budget/SKILL.md +1 -1
- package/skills/eacn3-budget-zh/SKILL.md +1 -1
- package/skills/eacn3-clarify/SKILL.md +1 -1
- package/skills/eacn3-clarify-zh/SKILL.md +1 -1
- package/skills/eacn3-collect/SKILL.md +1 -1
- package/skills/eacn3-collect-zh/SKILL.md +1 -1
- package/skills/eacn3-dashboard/SKILL.md +3 -3
- package/skills/eacn3-dashboard-zh/SKILL.md +3 -3
- package/skills/eacn3-delegate/SKILL.md +1 -1
- package/skills/eacn3-delegate-zh/SKILL.md +1 -1
- package/skills/eacn3-execute/SKILL.md +1 -1
- package/skills/eacn3-execute-zh/SKILL.md +1 -1
- package/skills/eacn3-invite/SKILL.md +1 -1
- package/skills/eacn3-invite-zh/SKILL.md +1 -1
- package/skills/eacn3-task/SKILL.md +1 -1
- package/skills/eacn3-task-zh/SKILL.md +1 -1
package/dist/index.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
import { EACN3_DEFAULT_NETWORK_ENDPOINT, isTierEligible } from "./src/models.js";
|
|
8
8
|
import * as state from "./src/state.js";
|
|
9
9
|
import * as net from "./src/network-client.js";
|
|
10
|
-
import * as ws from "./src/
|
|
10
|
+
import * as ws from "./src/event-transport.js";
|
|
11
11
|
import * as a2a from "./src/a2a-server.js";
|
|
12
|
+
import * as rc from "./src/reverse-control.js";
|
|
12
13
|
// ---------------------------------------------------------------------------
|
|
13
14
|
// Heartbeat
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
@@ -33,7 +34,18 @@ function stopHeartbeat() {
|
|
|
33
34
|
// Helper
|
|
34
35
|
// ---------------------------------------------------------------------------
|
|
35
36
|
function ok(data) {
|
|
36
|
-
|
|
37
|
+
const result = {
|
|
38
|
+
content: [{ type: "text", text: JSON.stringify(data) }],
|
|
39
|
+
};
|
|
40
|
+
// Directive injection: in OpenClaw we have no MCP Server instance,
|
|
41
|
+
// so sampling/notifications are unavailable. Instead, piggyback
|
|
42
|
+
// pending event directives onto every tool response — the Host LLM
|
|
43
|
+
// sees them immediately and can act without explicit polling.
|
|
44
|
+
const directives = rc.drainDirectives();
|
|
45
|
+
if (directives) {
|
|
46
|
+
result.content.push({ type: "text", text: directives });
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
37
49
|
}
|
|
38
50
|
function err(message) {
|
|
39
51
|
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }] };
|
|
@@ -74,8 +86,10 @@ function resolveAgentId(provided) {
|
|
|
74
86
|
function registerEventCallbacks() {
|
|
75
87
|
ws.setEventCallback((agentId, event) => {
|
|
76
88
|
const taskId = event.task_id;
|
|
89
|
+
// Reverse control: try to handle event proactively
|
|
90
|
+
rc.handleEvent(agentId, event).catch(() => { });
|
|
77
91
|
switch (event.type) {
|
|
78
|
-
case "
|
|
92
|
+
case "task_collected":
|
|
79
93
|
state.updateTaskStatus(taskId, "awaiting_retrieval");
|
|
80
94
|
break;
|
|
81
95
|
case "subtask_completed": {
|
|
@@ -83,7 +97,8 @@ function registerEventCallbacks() {
|
|
|
83
97
|
if (subtaskId) {
|
|
84
98
|
net.getTaskResults(subtaskId, agentId)
|
|
85
99
|
.then((res) => {
|
|
86
|
-
state.pushEvents([{
|
|
100
|
+
state.pushEvents(agentId, [{
|
|
101
|
+
msg_id: crypto.randomUUID().replace(/-/g, ""),
|
|
87
102
|
type: "subtask_completed",
|
|
88
103
|
task_id: taskId,
|
|
89
104
|
payload: { subtask_id: subtaskId, results: res.results },
|
|
@@ -94,11 +109,11 @@ function registerEventCallbacks() {
|
|
|
94
109
|
}
|
|
95
110
|
break;
|
|
96
111
|
}
|
|
97
|
-
case "
|
|
112
|
+
case "task_timeout":
|
|
98
113
|
state.updateTaskStatus(taskId, "no_one");
|
|
99
114
|
net.reportEvent(agentId, "task_timeout").catch(() => { });
|
|
100
115
|
break;
|
|
101
|
-
case "
|
|
116
|
+
case "bid_request_confirmation":
|
|
102
117
|
break;
|
|
103
118
|
case "task_broadcast":
|
|
104
119
|
autoBidEvaluate(agentId, event).catch(() => { });
|
|
@@ -123,11 +138,13 @@ async function autoBidEvaluate(agentId, event) {
|
|
|
123
138
|
if (!isInvited && !isTierEligible(agentTier, taskLevel))
|
|
124
139
|
return;
|
|
125
140
|
if (agent.capabilities?.max_concurrent_tasks) {
|
|
126
|
-
|
|
141
|
+
// Filter by this agent's tasks only (#110)
|
|
142
|
+
const activeTasks = Object.values(state.getState().local_tasks).filter((t) => t.agent_id === agentId && t.role === "executor" && t.status !== "completed" && t.status !== "no_one");
|
|
127
143
|
if (activeTasks.length >= agent.capabilities.max_concurrent_tasks)
|
|
128
144
|
return;
|
|
129
145
|
}
|
|
130
|
-
state.pushEvents([{
|
|
146
|
+
state.pushEvents(agentId, [{
|
|
147
|
+
msg_id: crypto.randomUUID().replace(/-/g, ""),
|
|
131
148
|
type: "task_broadcast",
|
|
132
149
|
task_id: taskId,
|
|
133
150
|
payload: { ...payload, auto_match: true, matched_agent: agentId },
|
|
@@ -135,6 +152,119 @@ async function autoBidEvaluate(agentId, event) {
|
|
|
135
152
|
}]);
|
|
136
153
|
}
|
|
137
154
|
// ---------------------------------------------------------------------------
|
|
155
|
+
// Event helpers for eacn3_await_events
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
/**
|
|
158
|
+
* Drain events from the buffer, optionally filtering by type.
|
|
159
|
+
* Unlike state.drainEvents(), only removes matching events and leaves the rest.
|
|
160
|
+
*/
|
|
161
|
+
function drainMatchingEvents(agentId, filterTypes) {
|
|
162
|
+
const all = state.drainEvents(agentId);
|
|
163
|
+
if (!filterTypes || filterTypes.length === 0)
|
|
164
|
+
return all;
|
|
165
|
+
const matching = [];
|
|
166
|
+
const remaining = [];
|
|
167
|
+
for (const e of all) {
|
|
168
|
+
if (filterTypes.includes(e.type)) {
|
|
169
|
+
matching.push(e);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
remaining.push(e);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Put non-matching events back
|
|
176
|
+
if (remaining.length > 0)
|
|
177
|
+
state.pushEvents(agentId, remaining);
|
|
178
|
+
return matching;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Build the await_events response with suggested actions for each event.
|
|
182
|
+
* This is the core of "reverse control without sampling" — the tool result
|
|
183
|
+
* tells the LLM exactly what action to take.
|
|
184
|
+
*/
|
|
185
|
+
function buildAwaitResponse(events) {
|
|
186
|
+
return {
|
|
187
|
+
count: events.length,
|
|
188
|
+
events: events.map((event) => {
|
|
189
|
+
const payload = event.payload;
|
|
190
|
+
switch (event.type) {
|
|
191
|
+
case "task_broadcast":
|
|
192
|
+
return {
|
|
193
|
+
event,
|
|
194
|
+
suggested_action: `New task in domains [${(payload.domains ?? []).join(", ")}] with budget ${payload.budget ?? "?"}. Evaluate and submit a bid if it matches your capabilities.`,
|
|
195
|
+
suggested_tool: "eacn3_submit_bid",
|
|
196
|
+
suggested_params: {
|
|
197
|
+
task_id: event.task_id,
|
|
198
|
+
confidence: "0.0-1.0 (your self-assessed ability)",
|
|
199
|
+
price: "credits you want as payment",
|
|
200
|
+
},
|
|
201
|
+
urgency: "high",
|
|
202
|
+
};
|
|
203
|
+
case "direct_message":
|
|
204
|
+
return {
|
|
205
|
+
event,
|
|
206
|
+
suggested_action: `Agent ${payload.from ?? "unknown"} sent you a message: "${String(payload.content ?? "").slice(0, 200)}". Consider replying.`,
|
|
207
|
+
suggested_tool: "eacn3_send_message",
|
|
208
|
+
suggested_params: {
|
|
209
|
+
to_agent_id: payload.from,
|
|
210
|
+
content: "your reply here",
|
|
211
|
+
task_id: event.task_id,
|
|
212
|
+
},
|
|
213
|
+
urgency: "high",
|
|
214
|
+
};
|
|
215
|
+
case "subtask_completed":
|
|
216
|
+
return {
|
|
217
|
+
event,
|
|
218
|
+
suggested_action: `Subtask ${payload.subtask_id ?? "?"} completed for task ${event.task_id}. Fetch and review the results, then decide: submit your final result or create another subtask.`,
|
|
219
|
+
suggested_tool: "eacn3_get_task_results",
|
|
220
|
+
suggested_params: { task_id: String(payload.subtask_id ?? event.task_id) },
|
|
221
|
+
urgency: "high",
|
|
222
|
+
};
|
|
223
|
+
case "bid_request_confirmation":
|
|
224
|
+
return {
|
|
225
|
+
event,
|
|
226
|
+
suggested_action: `A bid on task ${event.task_id} exceeded the budget (bid: ${payload.price ?? "?"}, budget: ${payload.budget ?? "?"}). Approve or reject.`,
|
|
227
|
+
suggested_tool: "eacn3_confirm_budget",
|
|
228
|
+
suggested_params: { task_id: event.task_id, approved: true },
|
|
229
|
+
urgency: "high",
|
|
230
|
+
};
|
|
231
|
+
case "task_collected":
|
|
232
|
+
return {
|
|
233
|
+
event,
|
|
234
|
+
suggested_action: `Task ${event.task_id} has results ready. Retrieve and select the best one.`,
|
|
235
|
+
suggested_tool: "eacn3_get_task_results",
|
|
236
|
+
suggested_params: { task_id: event.task_id },
|
|
237
|
+
urgency: "medium",
|
|
238
|
+
};
|
|
239
|
+
case "discussion_update":
|
|
240
|
+
return {
|
|
241
|
+
event,
|
|
242
|
+
suggested_action: `New discussion message on task ${event.task_id}. Read and respond.`,
|
|
243
|
+
suggested_tool: "eacn3_get_task",
|
|
244
|
+
suggested_params: { task_id: event.task_id },
|
|
245
|
+
urgency: "medium",
|
|
246
|
+
};
|
|
247
|
+
case "task_timeout":
|
|
248
|
+
return {
|
|
249
|
+
event,
|
|
250
|
+
suggested_action: `Task ${event.task_id} timed out. Reputation impact was automatic. No action needed.`,
|
|
251
|
+
suggested_tool: "eacn3_get_task",
|
|
252
|
+
suggested_params: { task_id: event.task_id },
|
|
253
|
+
urgency: "low",
|
|
254
|
+
};
|
|
255
|
+
default:
|
|
256
|
+
return {
|
|
257
|
+
event,
|
|
258
|
+
suggested_action: `Unknown event type "${event.type}" on task ${event.task_id}. Inspect manually.`,
|
|
259
|
+
suggested_tool: "eacn3_get_task",
|
|
260
|
+
suggested_params: { task_id: event.task_id },
|
|
261
|
+
urgency: "low",
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
138
268
|
// Plugin entry
|
|
139
269
|
// ---------------------------------------------------------------------------
|
|
140
270
|
export default {
|
|
@@ -196,7 +326,7 @@ export default {
|
|
|
196
326
|
// #1 eacn3_connect
|
|
197
327
|
api.registerTool({
|
|
198
328
|
name: "eacn3_connect",
|
|
199
|
-
description: "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,
|
|
329
|
+
description: "Connect to the EACN3 network — this must be your FIRST call. Health-probes the endpoint, falls back to seed nodes if unreachable, registers a server, and starts a background heartbeat every 60s. Returns {server_id, network_endpoint, fallback, available_agents, hint}. IMPORTANT: agents are NOT auto-restored. Check available_agents — if you have a previous agent, call eacn3_claim_agent(agent_id) to resume it. Otherwise call eacn3_register_agent() to create a new one. Only one agent per session.",
|
|
200
330
|
parameters: {
|
|
201
331
|
type: "object",
|
|
202
332
|
properties: {
|
|
@@ -227,9 +357,9 @@ export default {
|
|
|
227
357
|
s.server_card.status = "online";
|
|
228
358
|
}
|
|
229
359
|
catch {
|
|
230
|
-
const res = await net.registerServer("0.
|
|
360
|
+
const res = await net.registerServer("0.5.1", "plugin://local", "plugin-user");
|
|
231
361
|
sid = res.server_id;
|
|
232
|
-
s.server_card = { server_id: sid, version: "0.
|
|
362
|
+
s.server_card = { server_id: sid, version: "0.5.1", endpoint: "plugin://local", owner: "plugin-user", status: "online" };
|
|
233
363
|
for (const agent of Object.values(s.agents)) {
|
|
234
364
|
agent.server_id = sid;
|
|
235
365
|
try {
|
|
@@ -240,34 +370,19 @@ export default {
|
|
|
240
370
|
}
|
|
241
371
|
}
|
|
242
372
|
else {
|
|
243
|
-
const res = await net.registerServer("0.
|
|
373
|
+
const res = await net.registerServer("0.5.1", "plugin://local", "plugin-user");
|
|
244
374
|
sid = res.server_id;
|
|
245
|
-
s.server_card = { server_id: sid, version: "0.
|
|
375
|
+
s.server_card = { server_id: sid, version: "0.5.1", endpoint: "plugin://local", owner: "plugin-user", status: "online" };
|
|
246
376
|
}
|
|
247
|
-
state.
|
|
377
|
+
state.saveServerData();
|
|
248
378
|
startHeartbeat();
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
try {
|
|
252
|
-
await net.getAgentInfo(agent.agent_id);
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
try {
|
|
256
|
-
await net.registerAgent(agent);
|
|
257
|
-
}
|
|
258
|
-
catch { /* best-effort */ }
|
|
259
|
-
}
|
|
260
|
-
ws.connect(agent.agent_id);
|
|
261
|
-
}
|
|
262
|
-
const restoredAgents = Object.values(s.agents).map((a) => ({
|
|
263
|
-
agent_id: a.agent_id, name: a.name, domains: a.domains, tier: a.tier,
|
|
264
|
-
}));
|
|
379
|
+
// List agents available on disk — do NOT auto-restore
|
|
380
|
+
const availableAgents = state.listAvailableAgents();
|
|
265
381
|
return ok({
|
|
266
382
|
connected: true, server_id: sid, network_endpoint: endpoint, fallback,
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
? "You have previously registered agents restored and reconnected. You can use them directly without re-registering. Call eacn3_list_my_agents() for full details."
|
|
383
|
+
available_agents: availableAgents,
|
|
384
|
+
hint: availableAgents.length > 0
|
|
385
|
+
? "Previous agents found on disk. Call eacn3_claim_agent(agent_id) to resume one, or eacn3_register_agent() to create a new one."
|
|
271
386
|
: "No previous agents found. Register a new agent with eacn3_register_agent().",
|
|
272
387
|
});
|
|
273
388
|
},
|
|
@@ -275,16 +390,16 @@ export default {
|
|
|
275
390
|
// #2 eacn3_disconnect
|
|
276
391
|
api.registerTool({
|
|
277
392
|
name: "eacn3_disconnect",
|
|
278
|
-
description: "Disconnect from the EACN3 network
|
|
393
|
+
description: "Disconnect from the EACN3 network. Requires: eacn3_connect first. Side effects: active tasks will timeout and hurt reputation. Server identity is preserved — on next eacn3_connect you can claim your agent back via eacn3_claim_agent. Returns {disconnected: true}. Only call at end of session.",
|
|
279
394
|
parameters: { type: "object", properties: {} },
|
|
280
395
|
async execute() {
|
|
281
396
|
stopHeartbeat();
|
|
282
397
|
ws.disconnectAll();
|
|
283
398
|
// Do NOT call unregisterServer — it cascade-deletes all agents on the network side.
|
|
284
399
|
const s = state.getState();
|
|
400
|
+
// Don't write server.json — other sessions may still be using this server.
|
|
285
401
|
if (s.server_card)
|
|
286
402
|
s.server_card.status = "offline";
|
|
287
|
-
state.save();
|
|
288
403
|
return ok({ disconnected: true });
|
|
289
404
|
},
|
|
290
405
|
});
|
|
@@ -347,10 +462,42 @@ export default {
|
|
|
347
462
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
348
463
|
// Agent Management (7)
|
|
349
464
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
465
|
+
// #4b eacn3_claim_agent
|
|
466
|
+
api.registerTool({
|
|
467
|
+
name: "eacn3_claim_agent",
|
|
468
|
+
description: "Claim a previously registered agent from disk into this session. Use this to resume an agent listed in available_agents from eacn3_connect. The agent is re-registered on the network and event transport is started. Only one agent per session.",
|
|
469
|
+
parameters: {
|
|
470
|
+
type: "object",
|
|
471
|
+
properties: {
|
|
472
|
+
agent_id: { type: "string", description: "ID of the agent to claim (from available_agents)" },
|
|
473
|
+
},
|
|
474
|
+
required: ["agent_id"],
|
|
475
|
+
},
|
|
476
|
+
async execute(_id, params) {
|
|
477
|
+
if (state.listAgents().length > 0) {
|
|
478
|
+
return err("This session already has an agent. Only one agent per session.");
|
|
479
|
+
}
|
|
480
|
+
const agent = state.claimAgent(params.agent_id);
|
|
481
|
+
if (!agent) {
|
|
482
|
+
return err(`Agent ${params.agent_id} not found on disk. Use eacn3_register_agent to create a new one.`);
|
|
483
|
+
}
|
|
484
|
+
const s = state.getState();
|
|
485
|
+
if (s.server_card)
|
|
486
|
+
agent.server_id = s.server_card.server_id;
|
|
487
|
+
try {
|
|
488
|
+
await net.registerAgent(agent);
|
|
489
|
+
}
|
|
490
|
+
catch { /* best-effort */ }
|
|
491
|
+
ws.connect(agent.agent_id);
|
|
492
|
+
rc.configure(agent.agent_id);
|
|
493
|
+
state.save();
|
|
494
|
+
return ok({ claimed: true, agent_id: agent.agent_id, name: agent.name, domains: agent.domains, tier: agent.tier });
|
|
495
|
+
},
|
|
496
|
+
});
|
|
350
497
|
// #5 eacn3_register_agent
|
|
351
498
|
api.registerTool({
|
|
352
499
|
name: "eacn3_register_agent",
|
|
353
|
-
description: "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
|
|
500
|
+
description: "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 registers it for on-demand event fetching (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').",
|
|
354
501
|
parameters: {
|
|
355
502
|
type: "object",
|
|
356
503
|
properties: {
|
|
@@ -397,10 +544,19 @@ export default {
|
|
|
397
544
|
const res = await net.registerAgent(card);
|
|
398
545
|
state.addAgent(card);
|
|
399
546
|
ws.connect(agentId);
|
|
547
|
+
// Configure reverse control — in OpenClaw mode, sampling is never
|
|
548
|
+
// available, so the engine relies on directive injection + long-polling.
|
|
549
|
+
rc.configure(agentId, { enabled: true, policies: {} });
|
|
400
550
|
return ok({
|
|
401
551
|
registered: true, agent_id: agentId, seeds: res.seeds, domains: params.domains,
|
|
402
552
|
url: agentUrl,
|
|
403
553
|
a2a_server: a2a.isRunning() ? { port: a2a.getServerPort() } : null,
|
|
554
|
+
reverse_control: {
|
|
555
|
+
enabled: true,
|
|
556
|
+
sampling_available: false,
|
|
557
|
+
mode: "openclaw",
|
|
558
|
+
hint: "Use eacn3_await_events for reactive event handling. Directive injection is active on all tool responses.",
|
|
559
|
+
},
|
|
404
560
|
});
|
|
405
561
|
},
|
|
406
562
|
});
|
|
@@ -451,11 +607,12 @@ export default {
|
|
|
451
607
|
// #8 eacn3_unregister_agent
|
|
452
608
|
api.registerTool({
|
|
453
609
|
name: "eacn3_unregister_agent",
|
|
454
|
-
description: "Remove an agent from the network
|
|
610
|
+
description: "Remove an agent from the network. Side effects: deletes agent from local state. Active tasks assigned to this agent will timeout and hurt reputation. Returns {unregistered: true, agent_id}.",
|
|
455
611
|
parameters: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] },
|
|
456
612
|
async execute(_id, params) {
|
|
457
613
|
const res = await net.unregisterAgent(params.agent_id);
|
|
458
614
|
ws.disconnect(params.agent_id);
|
|
615
|
+
rc.unconfigure(params.agent_id);
|
|
459
616
|
state.removeAgent(params.agent_id);
|
|
460
617
|
// Stop A2A server if no agents remain
|
|
461
618
|
if (state.listAgents().length === 0 && a2a.isRunning()) {
|
|
@@ -467,11 +624,11 @@ export default {
|
|
|
467
624
|
// #9 eacn3_list_my_agents
|
|
468
625
|
api.registerTool({
|
|
469
626
|
name: "eacn3_list_my_agents",
|
|
470
|
-
description: "List all agents registered on this local server instance. Returns {count, agents[]} where each agent includes agent_id, name, domains, tier, and
|
|
627
|
+
description: "List all agents registered on this local server instance. Returns {count, agents[]} where each agent includes agent_id, name, domains, tier, and registered status. No network call — reads local state only. Use to check which agents are active.",
|
|
471
628
|
parameters: { type: "object", properties: {} },
|
|
472
629
|
async execute() {
|
|
473
630
|
const agents = state.listAgents();
|
|
474
|
-
return ok({ count: agents.length, agents: agents.map((a) => ({ agent_id: a.agent_id, name: a.name, domains: a.domains, tier: a.tier,
|
|
631
|
+
return ok({ count: agents.length, agents: agents.map((a) => ({ agent_id: a.agent_id, name: a.name, domains: a.domains, tier: a.tier, registered: ws.isConnected(a.agent_id) })) });
|
|
475
632
|
},
|
|
476
633
|
});
|
|
477
634
|
// #10 eacn3_discover_agents
|
|
@@ -591,7 +748,7 @@ export default {
|
|
|
591
748
|
level: params.level ?? "general",
|
|
592
749
|
invited_agent_ids: params.invited_agent_ids,
|
|
593
750
|
});
|
|
594
|
-
state.updateTask({ task_id: taskId, role: "initiator", status: task.status, domains: params.domains ?? [], description_summary: params.description.slice(0, 100), created_at: new Date().toISOString() });
|
|
751
|
+
state.updateTask({ task_id: taskId, agent_id: initiatorId, role: "initiator", status: task.status, domains: params.domains ?? [], description_summary: params.description.slice(0, 100), created_at: new Date().toISOString() });
|
|
595
752
|
return ok({ task_id: taskId, status: task.status, budget: params.budget, local_matches: matchedLocal.map((a) => a.agent_id) });
|
|
596
753
|
},
|
|
597
754
|
});
|
|
@@ -626,14 +783,14 @@ export default {
|
|
|
626
783
|
// #21 eacn3_update_discussions
|
|
627
784
|
api.registerTool({
|
|
628
785
|
name: "eacn3_update_discussions",
|
|
629
|
-
description: "Post a clarification or discussion message on a task visible to all bidders. Requires: you must be the task initiator. Side effects: triggers a '
|
|
786
|
+
description: "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.",
|
|
630
787
|
parameters: { type: "object", properties: { task_id: { type: "string" }, message: { type: "string" }, initiator_id: { type: "string", description: "Initiator agent ID (auto-injected if omitted)" } }, required: ["task_id", "message"] },
|
|
631
788
|
async execute(_id, params) { const initiatorId = resolveAgentId(params.initiator_id); return ok(await net.updateDiscussions(params.task_id, initiatorId, params.message)); },
|
|
632
789
|
});
|
|
633
790
|
// #22 eacn3_confirm_budget
|
|
634
791
|
api.registerTool({
|
|
635
792
|
name: "eacn3_confirm_budget",
|
|
636
|
-
description: "Approve or reject a bid that exceeded your task's budget, triggered by a '
|
|
793
|
+
description: "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.",
|
|
637
794
|
parameters: { type: "object", properties: { task_id: { type: "string" }, approved: { type: "boolean" }, new_budget: { type: "number" }, initiator_id: { type: "string", description: "Initiator agent ID (auto-injected if omitted)" } }, required: ["task_id", "approved"] },
|
|
638
795
|
async execute(_id, params) { const initiatorId = resolveAgentId(params.initiator_id); return ok(await net.confirmBudget(params.task_id, initiatorId, params.approved, params.new_budget)); },
|
|
639
796
|
});
|
|
@@ -688,7 +845,7 @@ export default {
|
|
|
688
845
|
// Tier/level filtering and invite bypass are handled server-side in matcher.check_bid().
|
|
689
846
|
const res = await net.submitBid(params.task_id, agentId, params.confidence, params.price);
|
|
690
847
|
if (res.status && res.status !== "rejected") {
|
|
691
|
-
state.updateTask({ task_id: params.task_id, role: "executor", status: "bidding", domains: [], description_summary: "", created_at: new Date().toISOString() });
|
|
848
|
+
state.updateTask({ task_id: params.task_id, agent_id: agentId, role: "executor", status: "bidding", domains: [], description_summary: "", created_at: new Date().toISOString() });
|
|
692
849
|
}
|
|
693
850
|
return ok(res);
|
|
694
851
|
}),
|
|
@@ -754,6 +911,7 @@ export default {
|
|
|
754
911
|
const senderId = resolveAgentId(params.sender_id);
|
|
755
912
|
const targetId = params.agent_id;
|
|
756
913
|
const message = {
|
|
914
|
+
msg_id: crypto.randomUUID().replace(/-/g, ""),
|
|
757
915
|
type: "direct_message",
|
|
758
916
|
task_id: "",
|
|
759
917
|
payload: { from: senderId, content: params.content },
|
|
@@ -762,7 +920,7 @@ export default {
|
|
|
762
920
|
// Local agent — direct push to event buffer
|
|
763
921
|
const localAgent = state.getAgent(targetId);
|
|
764
922
|
if (localAgent) {
|
|
765
|
-
state.pushEvents([message]);
|
|
923
|
+
state.pushEvents(targetId, [message]);
|
|
766
924
|
return ok({ sent: true, to: targetId, from: senderId, local: true });
|
|
767
925
|
}
|
|
768
926
|
// Remote agent — POST to their URL callback (A2A direct, agent.md:160-168)
|
|
@@ -849,11 +1007,64 @@ export default {
|
|
|
849
1007
|
// #32 eacn3_get_events
|
|
850
1008
|
api.registerTool({
|
|
851
1009
|
name: "eacn3_get_events",
|
|
852
|
-
description: "
|
|
1010
|
+
description: "Fetch pending events from the network for a specific agent, plus any locally buffered synthetic events. Returns {count, events[], reverse_control} where event types include: task_broadcast, bid_request_confirmation, bid_result, discussion_update, subtask_completed, task_collected, task_timeout, adjudication_task, direct_message.",
|
|
1011
|
+
parameters: { type: "object", properties: { agent_id: { type: "string", description: "Agent ID to drain events for (auto-injected if omitted)" } } },
|
|
1012
|
+
async execute(_id, params) {
|
|
1013
|
+
const agentId = resolveAgentId(params.agent_id);
|
|
1014
|
+
const networkEvents = await ws.fetchEvents(agentId, 0);
|
|
1015
|
+
const localEvents = state.drainEvents(agentId);
|
|
1016
|
+
const events = [...networkEvents, ...localEvents].filter((e) => !e._handled);
|
|
1017
|
+
return ok({ count: events.length, events, reverse_control: rc.getStatus() });
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
1020
|
+
// #39 eacn3_await_events — on-demand event fetching
|
|
1021
|
+
api.registerTool({
|
|
1022
|
+
name: "eacn3_await_events",
|
|
1023
|
+
description: "Fetch events from the network with a configurable wait time. First checks locally buffered synthetic events, then does a single long-poll to the network. Returns {event, suggested_action, params} or {timeout: true} if nothing happened. Prefer this over eacn3_get_events for reactive agent loops.",
|
|
1024
|
+
parameters: {
|
|
1025
|
+
type: "object",
|
|
1026
|
+
properties: {
|
|
1027
|
+
agent_id: { type: "string", description: "Agent ID to await events for (auto-injected if omitted)" },
|
|
1028
|
+
timeout_seconds: { type: "number", description: "Max seconds to wait. Default 30, max 120." },
|
|
1029
|
+
event_types: { type: "array", items: { type: "string" }, description: "Only return for these event types. Default: all types." },
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
1032
|
+
async execute(_id, params) {
|
|
1033
|
+
const agentId = resolveAgentId(params.agent_id);
|
|
1034
|
+
const timeoutSec = Math.min(Math.max(params.timeout_seconds ?? 30, 1), 120);
|
|
1035
|
+
const filterTypes = params.event_types;
|
|
1036
|
+
// Check locally buffered synthetic events first
|
|
1037
|
+
const immediate = drainMatchingEvents(agentId, filterTypes).filter((e) => !e._handled);
|
|
1038
|
+
if (immediate.length > 0) {
|
|
1039
|
+
return ok(buildAwaitResponse(immediate));
|
|
1040
|
+
}
|
|
1041
|
+
// Single long-poll to network with the agent's requested timeout
|
|
1042
|
+
const networkEvents = await ws.fetchEvents(agentId, timeoutSec);
|
|
1043
|
+
const localAfter = drainMatchingEvents(agentId, filterTypes);
|
|
1044
|
+
const all = [...networkEvents, ...localAfter].filter((e) => !e._handled);
|
|
1045
|
+
// Filter by event types if specified
|
|
1046
|
+
const filtered = filterTypes && filterTypes.length > 0
|
|
1047
|
+
? all.filter((e) => filterTypes.includes(e.type))
|
|
1048
|
+
: all;
|
|
1049
|
+
// Put back non-matching events
|
|
1050
|
+
if (filterTypes && filterTypes.length > 0) {
|
|
1051
|
+
const remaining = all.filter((e) => !filterTypes.includes(e.type));
|
|
1052
|
+
if (remaining.length > 0)
|
|
1053
|
+
state.pushEvents(agentId, remaining);
|
|
1054
|
+
}
|
|
1055
|
+
if (filtered.length === 0) {
|
|
1056
|
+
return ok({ timeout: true, waited_seconds: timeoutSec, hint: "No events arrived. Call again to keep waiting, or proceed with other work." });
|
|
1057
|
+
}
|
|
1058
|
+
return ok(buildAwaitResponse(filtered));
|
|
1059
|
+
},
|
|
1060
|
+
});
|
|
1061
|
+
// #40 eacn3_reverse_control_status
|
|
1062
|
+
api.registerTool({
|
|
1063
|
+
name: "eacn3_reverse_control_status",
|
|
1064
|
+
description: "Get the current status of the MCP reverse control engine. Shows whether sampling is available (always false in OpenClaw — use eacn3_await_events instead), configured agents, pending directive count, and rate limiting info.",
|
|
853
1065
|
parameters: { type: "object", properties: {} },
|
|
854
1066
|
async execute() {
|
|
855
|
-
|
|
856
|
-
return ok({ count: events.length, events });
|
|
1067
|
+
return ok({ ...rc.getStatus(), openclaw_mode: true, recommended_tool: "eacn3_await_events" });
|
|
857
1068
|
},
|
|
858
1069
|
});
|
|
859
1070
|
},
|