multiclaws 0.4.36 → 0.4.38
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/README.md +39 -39
- package/README.zh-CN.md +39 -39
- package/dist/gateway/handlers.js +17 -0
- package/dist/index.js +89 -45
- package/dist/infra/version.d.ts +1 -0
- package/dist/infra/version.js +19 -0
- package/dist/service/a2a-adapter.d.ts +27 -4
- package/dist/service/a2a-adapter.js +239 -30
- package/dist/service/multiclaws-service.d.ts +7 -0
- package/dist/service/multiclaws-service.js +84 -20
- package/openclaw.plugin.json +47 -47
- package/package.json +56 -56
- package/skills/meeting-scheduler/SKILL.md +175 -0
- package/skills/multiclaws/SKILL.md +218 -218
|
@@ -2,6 +2,53 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OpenClawAgentExecutor = void 0;
|
|
4
4
|
const gateway_client_1 = require("../infra/gateway-client");
|
|
5
|
+
/* ------------------------------------------------------------------ */
|
|
6
|
+
/* Risk classification */
|
|
7
|
+
/* ------------------------------------------------------------------ */
|
|
8
|
+
/**
|
|
9
|
+
* Heuristic risk classifier. Returns "safe" only when the task is
|
|
10
|
+
* clearly a read-only query; defaults to "risky" for anything ambiguous.
|
|
11
|
+
*
|
|
12
|
+
* This drives the permission gate: risky tasks require explicit human
|
|
13
|
+
* approval before a sub-agent is spawned to execute them.
|
|
14
|
+
*/
|
|
15
|
+
function classifyTaskRisk(taskText) {
|
|
16
|
+
const text = taskText.toLowerCase();
|
|
17
|
+
// Explicit risky patterns (write / modify / execute / send)
|
|
18
|
+
const riskyPatterns = [
|
|
19
|
+
// English — word-boundary matched to avoid false positives
|
|
20
|
+
/\b(write|creat|delet|remov|modif|edit|updat|install|execut|deploy|push|commit|send|post|drop|format|rename|overwrite|reset|wipe|destroy|kill|terminat|rm|mkdir|touch|mv)\b/i,
|
|
21
|
+
// Chinese — multi-character phrases to avoid single-char false positives
|
|
22
|
+
// e.g. 安 alone would match 安排(schedule) or 安全(safe)
|
|
23
|
+
/写入|写文件|写邮件|写信|创建|新建|删除|移除|修改|更改|编辑|更新|升级|安装|部署|执行|运行命令|发送|发邮件|提交|推送|重命名|覆盖|重置|清空|清除|销毁|终止|停止服务|kill进程/,
|
|
24
|
+
];
|
|
25
|
+
// Explicitly safe read-only patterns — checked BEFORE risky to avoid false positives
|
|
26
|
+
// (e.g. "查询并发送报告" is risky overall, but "查询" alone should be safe)
|
|
27
|
+
const safePatterns = [
|
|
28
|
+
// English read-only verbs
|
|
29
|
+
/\b(list|show|get|check|view|read|query|find|search|display|fetch|retriev|look|what|which|count|how many|summariz|describ|explain|analyz|report)\b/i,
|
|
30
|
+
// Chinese read-only verbs (multi-char to be specific)
|
|
31
|
+
/查看|查询|获取|搜索|显示|检查|列出|列举|统计|描述|分析|报告|读取|浏览/,
|
|
32
|
+
// Calendar / scheduling queries
|
|
33
|
+
/\b(calendar|schedule|event|meeting|free|busy|availab|appointment)\b/i,
|
|
34
|
+
/日历|日程|会议|空闲|忙碌|可用时间|时间段|什么时候|哪个时间|安排会议|约会|预约/,
|
|
35
|
+
// Process / system info queries
|
|
36
|
+
/\b(process|pid|cpu|memory|disk|uptime|version|status|running|service|log)\b/i,
|
|
37
|
+
/进程|内存|磁盘|系统状态|运行状态|版本信息|日志|监控/,
|
|
38
|
+
];
|
|
39
|
+
// Safe check first — if clearly a read query, don't let ambiguous chars trigger risky
|
|
40
|
+
if (safePatterns.some((p) => p.test(text))) {
|
|
41
|
+
return "safe";
|
|
42
|
+
}
|
|
43
|
+
if (riskyPatterns.some((p) => p.test(text))) {
|
|
44
|
+
return "risky";
|
|
45
|
+
}
|
|
46
|
+
// Default: treat as risky if uncertain
|
|
47
|
+
return "risky";
|
|
48
|
+
}
|
|
49
|
+
/* ------------------------------------------------------------------ */
|
|
50
|
+
/* Helpers */
|
|
51
|
+
/* ------------------------------------------------------------------ */
|
|
5
52
|
function extractTextFromMessage(message) {
|
|
6
53
|
if (!message.parts)
|
|
7
54
|
return "";
|
|
@@ -15,22 +62,28 @@ function extractTextFromMessage(message) {
|
|
|
15
62
|
*
|
|
16
63
|
* When a remote agent sends a task via A2A `message/send`,
|
|
17
64
|
* this executor:
|
|
18
|
-
* 1.
|
|
19
|
-
* 2.
|
|
20
|
-
* 3.
|
|
21
|
-
*
|
|
65
|
+
* 1. Classifies the task risk (safe vs risky)
|
|
66
|
+
* 2. Notifies the local human owner
|
|
67
|
+
* 3. For risky tasks: waits for explicit human approval
|
|
68
|
+
* For safe tasks: executes immediately
|
|
69
|
+
* 4. Calls OpenClaw's `sessions_spawn` (run mode) to start execution
|
|
70
|
+
* 5. Waits for the sub-agent to call back via `multiclaws_a2a_callback`
|
|
71
|
+
* 6. Returns the final result as a Message
|
|
22
72
|
*/
|
|
23
73
|
class OpenClawAgentExecutor {
|
|
24
74
|
gatewayConfig;
|
|
25
75
|
taskTracker;
|
|
26
76
|
getNotificationTargets;
|
|
77
|
+
registerDiscoveredTarget;
|
|
27
78
|
logger;
|
|
28
79
|
cwd;
|
|
29
80
|
pendingCallbacks = new Map();
|
|
81
|
+
pendingApprovals = new Map();
|
|
30
82
|
constructor(options) {
|
|
31
83
|
this.gatewayConfig = options.gatewayConfig;
|
|
32
84
|
this.taskTracker = options.taskTracker;
|
|
33
85
|
this.getNotificationTargets = options.getNotificationTargets ?? (() => new Map());
|
|
86
|
+
this.registerDiscoveredTarget = options.registerDiscoveredTarget;
|
|
34
87
|
this.logger = options.logger;
|
|
35
88
|
this.cwd = options.cwd || process.cwd();
|
|
36
89
|
}
|
|
@@ -61,10 +114,49 @@ class OpenClawAgentExecutor {
|
|
|
61
114
|
eventBus.finished();
|
|
62
115
|
return;
|
|
63
116
|
}
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
this.logger.info(`[a2a-adapter] task ${taskId}
|
|
67
|
-
|
|
117
|
+
// Classify risk and gate accordingly
|
|
118
|
+
const risk = classifyTaskRisk(taskText);
|
|
119
|
+
this.logger.info(`[a2a-adapter] task ${taskId} risk=${risk}`);
|
|
120
|
+
if (risk === "risky") {
|
|
121
|
+
// Notify with approval request and wait
|
|
122
|
+
const approvalTimeoutMs = 5 * 60 * 1000; // 5 minutes
|
|
123
|
+
const approvalPromise = this.createApprovalCallback(taskId, approvalTimeoutMs);
|
|
124
|
+
this.logger.info(`[a2a-adapter] task ${taskId} requesting human approval (timeout=${approvalTimeoutMs / 1000}s)`);
|
|
125
|
+
void this.notifyUser(buildApprovalRequest(taskId, fromAgentName, taskText));
|
|
126
|
+
let approved;
|
|
127
|
+
try {
|
|
128
|
+
approved = await approvalPromise;
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
const isCanceled = err instanceof Error && err.message === "canceled";
|
|
132
|
+
if (isCanceled) {
|
|
133
|
+
// Task was explicitly canceled — use the canonical "canceled" message
|
|
134
|
+
this.logger.info(`[a2a-adapter] task ${taskId} canceled during approval wait`);
|
|
135
|
+
this.taskTracker.update(taskId, { status: "failed", error: "canceled" });
|
|
136
|
+
this.publishMessage(eventBus, "Task was canceled.");
|
|
137
|
+
eventBus.finished();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Approval timed out → auto-reject
|
|
141
|
+
approved = false;
|
|
142
|
+
this.logger.warn(`[a2a-adapter] task ${taskId} approval timed out — auto-rejected`);
|
|
143
|
+
}
|
|
144
|
+
if (!approved) {
|
|
145
|
+
const reason = "用户拒绝或未在超时时间内授权。";
|
|
146
|
+
this.logger.info(`[a2a-adapter] task ${taskId} rejected`);
|
|
147
|
+
this.taskTracker.update(taskId, { status: "failed", error: reason });
|
|
148
|
+
this.publishMessage(eventBus, `任务已被拒绝:${reason}`);
|
|
149
|
+
eventBus.finished();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.logger.info(`[a2a-adapter] task ${taskId} approved by user`);
|
|
153
|
+
void this.notifyUser(`✅ 已授权,开始执行来自 **${fromAgentName}** 的任务…`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
// Safe task: notify but auto-execute
|
|
157
|
+
this.logger.info(`[a2a-adapter] task ${taskId} safe query — auto-executing`);
|
|
158
|
+
void this.notifyUser(`📨 收到来自 **${fromAgentName}** 的查询任务(安全,自动执行):\n\n${taskText.slice(0, 300)}`);
|
|
159
|
+
}
|
|
68
160
|
try {
|
|
69
161
|
// Create a promise that resolves when sub-agent calls multiclaws_a2a_callback
|
|
70
162
|
const timeoutMs = 180_000;
|
|
@@ -88,15 +180,17 @@ class OpenClawAgentExecutor {
|
|
|
88
180
|
this.logger.info(`[a2a-adapter] task ${taskId} waiting for callback from sub-agent...`);
|
|
89
181
|
// Wait for the sub-agent to call back
|
|
90
182
|
const output = await resultPromise;
|
|
91
|
-
// Return result
|
|
183
|
+
// Return result and notify user
|
|
92
184
|
this.taskTracker.update(taskId, { status: "completed", result: output });
|
|
93
185
|
this.logger.info(`[a2a-adapter] ✓ task ${taskId} completed — resultLen=${output.length}, preview=${output.slice(0, 120)}`);
|
|
186
|
+
void this.notifyUser(`✅ **来自 ${fromAgentName} 的任务已完成**\n\n${output.slice(0, 800)}`);
|
|
94
187
|
this.publishMessage(eventBus, output || "Task completed with no output.");
|
|
95
188
|
}
|
|
96
189
|
catch (err) {
|
|
97
190
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
98
191
|
this.logger.error(`[a2a-adapter] ✗ task ${taskId} failed: ${errorMsg}`);
|
|
99
192
|
this.taskTracker.update(taskId, { status: "failed", error: errorMsg });
|
|
193
|
+
void this.notifyUser(`❌ 来自 **${fromAgentName}** 的任务执行失败:${errorMsg}`);
|
|
100
194
|
this.publishMessage(eventBus, `Error: ${errorMsg}`);
|
|
101
195
|
}
|
|
102
196
|
this.logger.info(`[a2a-adapter] task ${taskId} eventBus.finished()`);
|
|
@@ -118,8 +212,32 @@ class OpenClawAgentExecutor {
|
|
|
118
212
|
pending.resolve(result);
|
|
119
213
|
return true;
|
|
120
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Called when the local human owner approves or rejects a pending risky task.
|
|
217
|
+
* Returns true if a pending approval was found.
|
|
218
|
+
*/
|
|
219
|
+
resolveApproval(taskId, approved) {
|
|
220
|
+
const pending = this.pendingApprovals.get(taskId);
|
|
221
|
+
if (!pending) {
|
|
222
|
+
this.logger.warn(`[a2a-adapter] resolveApproval: no pending approval for taskId=${taskId}`);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
clearTimeout(pending.timer);
|
|
226
|
+
this.pendingApprovals.delete(taskId);
|
|
227
|
+
this.logger.info(`[a2a-adapter] resolveApproval: taskId=${taskId} approved=${approved}`);
|
|
228
|
+
pending.resolve(approved);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
121
231
|
async cancelTask(taskId, eventBus) {
|
|
122
232
|
this.logger.info(`[a2a-adapter] cancelTask(taskId=${taskId})`);
|
|
233
|
+
// Reject pending approval if any — distinct from user-rejection, uses Error("canceled")
|
|
234
|
+
const approval = this.pendingApprovals.get(taskId);
|
|
235
|
+
if (approval) {
|
|
236
|
+
clearTimeout(approval.timer);
|
|
237
|
+
this.pendingApprovals.delete(taskId);
|
|
238
|
+
approval.reject(new Error("canceled"));
|
|
239
|
+
this.logger.info(`[a2a-adapter] cancelTask: pending approval canceled for taskId=${taskId}`);
|
|
240
|
+
}
|
|
123
241
|
// Reject pending callback if any
|
|
124
242
|
const pending = this.pendingCallbacks.get(taskId);
|
|
125
243
|
if (pending) {
|
|
@@ -149,11 +267,78 @@ class OpenClawAgentExecutor {
|
|
|
149
267
|
this.pendingCallbacks.set(taskId, { resolve, reject, timer });
|
|
150
268
|
});
|
|
151
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Create a pending approval that resolves when the human owner responds,
|
|
272
|
+
* or rejects on timeout or cancellation.
|
|
273
|
+
*/
|
|
274
|
+
createApprovalCallback(taskId, timeoutMs) {
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
const timer = setTimeout(() => {
|
|
277
|
+
this.pendingApprovals.delete(taskId);
|
|
278
|
+
this.logger.warn(`[a2a-adapter] task ${taskId} approval timed out after ${timeoutMs / 1000}s`);
|
|
279
|
+
reject(new Error(`approval timed out after ${timeoutMs / 1000}s`));
|
|
280
|
+
}, timeoutMs);
|
|
281
|
+
this.pendingApprovals.set(taskId, { resolve, reject, timer });
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Discover the most recently active non-internal session via sessions_list.
|
|
286
|
+
* Used as fallback when no notification targets have been registered yet
|
|
287
|
+
* (e.g. right after a gateway restart before the user sends their first message).
|
|
288
|
+
*/
|
|
289
|
+
async discoverActiveSession() {
|
|
290
|
+
if (!this.gatewayConfig)
|
|
291
|
+
return null;
|
|
292
|
+
try {
|
|
293
|
+
const result = await (0, gateway_client_1.invokeGatewayTool)({
|
|
294
|
+
gateway: this.gatewayConfig,
|
|
295
|
+
tool: "sessions_list",
|
|
296
|
+
args: { limit: 10, activeMinutes: 120 },
|
|
297
|
+
timeoutMs: 5_000,
|
|
298
|
+
});
|
|
299
|
+
const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
|
|
300
|
+
const session = result?.sessions?.find((s) => s.sessionKey &&
|
|
301
|
+
!INTERNAL_PREFIXES.some((p) => s.sessionKey.startsWith(p)));
|
|
302
|
+
return session?.sessionKey ?? null;
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
this.logger.warn(`[a2a-adapter] discoverActiveSession failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
152
309
|
/** Send a notification to all known targets. Individual failures are silently ignored. */
|
|
153
310
|
async notifyUser(message) {
|
|
154
311
|
const targets = this.getNotificationTargets();
|
|
155
|
-
if (!this.gatewayConfig
|
|
156
|
-
this.logger.info(`[a2a-adapter] notifyUser: skipped (gateway
|
|
312
|
+
if (!this.gatewayConfig) {
|
|
313
|
+
this.logger.info(`[a2a-adapter] notifyUser: skipped (no gateway config)`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// Fallback: no registered targets yet (e.g. right after gateway restart).
|
|
317
|
+
// Discover the active session and send directly via sessions_send.
|
|
318
|
+
if (targets.size === 0) {
|
|
319
|
+
this.logger.info(`[a2a-adapter] notifyUser: no registered targets — attempting session discovery`);
|
|
320
|
+
const sessionKey = await this.discoverActiveSession();
|
|
321
|
+
if (sessionKey) {
|
|
322
|
+
this.logger.info(`[a2a-adapter] notifyUser: discovered session ${sessionKey}, sending via sessions_send`);
|
|
323
|
+
try {
|
|
324
|
+
await (0, gateway_client_1.invokeGatewayTool)({
|
|
325
|
+
gateway: this.gatewayConfig,
|
|
326
|
+
tool: "sessions_send",
|
|
327
|
+
args: { sessionKey, message },
|
|
328
|
+
timeoutMs: 5_000,
|
|
329
|
+
});
|
|
330
|
+
// Also register this session for future notifications
|
|
331
|
+
if (this.registerDiscoveredTarget) {
|
|
332
|
+
this.registerDiscoveredTarget(sessionKey);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch (err) {
|
|
336
|
+
this.logger.warn(`[a2a-adapter] notifyUser: sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
this.logger.warn(`[a2a-adapter] notifyUser: no active session found, message lost`);
|
|
341
|
+
}
|
|
157
342
|
return;
|
|
158
343
|
}
|
|
159
344
|
const results = await Promise.allSettled([...targets.entries()].map(async ([key, target]) => {
|
|
@@ -167,8 +352,10 @@ class OpenClawAgentExecutor {
|
|
|
167
352
|
timeoutMs: 5_000,
|
|
168
353
|
})
|
|
169
354
|
: (0, gateway_client_1.invokeGatewayTool)({
|
|
355
|
+
// sessions_send injects a message into the session so the AI
|
|
356
|
+
// can relay it to the human (correct tool; was "chat.send" before)
|
|
170
357
|
gateway: this.gatewayConfig,
|
|
171
|
-
tool: "
|
|
358
|
+
tool: "sessions_send",
|
|
172
359
|
args: { sessionKey: target.sessionKey, message },
|
|
173
360
|
timeoutMs: 5_000,
|
|
174
361
|
}));
|
|
@@ -194,28 +381,50 @@ class OpenClawAgentExecutor {
|
|
|
194
381
|
}
|
|
195
382
|
}
|
|
196
383
|
exports.OpenClawAgentExecutor = OpenClawAgentExecutor;
|
|
384
|
+
/* ------------------------------------------------------------------ */
|
|
385
|
+
/* Prompt builders */
|
|
386
|
+
/* ------------------------------------------------------------------ */
|
|
387
|
+
/**
|
|
388
|
+
* Build the approval request message injected into the human's active session.
|
|
389
|
+
* The AI in that session will relay it and handle the human's approve/reject response.
|
|
390
|
+
*/
|
|
391
|
+
function buildApprovalRequest(taskId, fromAgentName, taskText) {
|
|
392
|
+
const preview = taskText.length > 600 ? taskText.slice(0, 600) + "…" : taskText;
|
|
393
|
+
return `[MultiClaws] 收到来自 **${fromAgentName}** 的委派任务,需要授权
|
|
394
|
+
|
|
395
|
+
**任务内容:**
|
|
396
|
+
${preview}
|
|
397
|
+
|
|
398
|
+
⚠️ 该任务涉及写操作或高风险操作,需要您授权才能执行。
|
|
399
|
+
|
|
400
|
+
请询问用户是否同意执行,并根据回复调用对应工具:
|
|
401
|
+
- 同意:\`multiclaws_task_respond(taskId="${taskId}", approved=true)\`
|
|
402
|
+
- 拒绝:\`multiclaws_task_respond(taskId="${taskId}", approved=false)\`
|
|
403
|
+
|
|
404
|
+
授权等待时间:5 分钟,超时自动拒绝。`;
|
|
405
|
+
}
|
|
197
406
|
/**
|
|
198
407
|
* Build the prompt for the sub-agent that handles an incoming A2A task.
|
|
199
408
|
* The sub-agent must call `multiclaws_a2a_callback` to report its result.
|
|
200
409
|
*/
|
|
201
410
|
function buildA2ASubagentPrompt(taskId, taskText) {
|
|
202
|
-
return `你收到了一个来自远端智能体的委派任务。请完成任务并汇报结果。
|
|
203
|
-
|
|
204
|
-
## 任务内容
|
|
205
|
-
|
|
206
|
-
${taskText}
|
|
207
|
-
|
|
208
|
-
## 完成后必做
|
|
209
|
-
|
|
210
|
-
完成任务后,你**必须**调用 \`multiclaws_a2a_callback\` 工具汇报结果:
|
|
211
|
-
|
|
212
|
-
\`\`\`
|
|
213
|
-
multiclaws_a2a_callback(taskId="${taskId}", result="你的完整回复内容")
|
|
214
|
-
\`\`\`
|
|
215
|
-
|
|
216
|
-
**重要**:
|
|
217
|
-
- 无论任务成功还是失败,都必须调用 \`multiclaws_a2a_callback\`
|
|
218
|
-
- result 参数填写你的完整回复文本
|
|
219
|
-
- 如果任务失败,在 result 中说明失败原因
|
|
411
|
+
return `你收到了一个来自远端智能体的委派任务。请完成任务并汇报结果。
|
|
412
|
+
|
|
413
|
+
## 任务内容
|
|
414
|
+
|
|
415
|
+
${taskText}
|
|
416
|
+
|
|
417
|
+
## 完成后必做
|
|
418
|
+
|
|
419
|
+
完成任务后,你**必须**调用 \`multiclaws_a2a_callback\` 工具汇报结果:
|
|
420
|
+
|
|
421
|
+
\`\`\`
|
|
422
|
+
multiclaws_a2a_callback(taskId="${taskId}", result="你的完整回复内容")
|
|
423
|
+
\`\`\`
|
|
424
|
+
|
|
425
|
+
**重要**:
|
|
426
|
+
- 无论任务成功还是失败,都必须调用 \`multiclaws_a2a_callback\`
|
|
427
|
+
- result 参数填写你的完整回复文本
|
|
428
|
+
- 如果任务失败,在 result 中说明失败原因
|
|
220
429
|
- 这是唯一的结果回传方式,不调用则结果会丢失`;
|
|
221
430
|
}
|
|
@@ -133,12 +133,19 @@ export declare class MulticlawsService extends EventEmitter {
|
|
|
133
133
|
private extractArtifactText;
|
|
134
134
|
/** Fetch with up to 2 retries and exponential backoff. */
|
|
135
135
|
private fetchWithRetry;
|
|
136
|
+
/**
|
|
137
|
+
* Called by the `multiclaws_task_respond` tool when the local human
|
|
138
|
+
* approves or rejects a pending risky incoming task.
|
|
139
|
+
*/
|
|
140
|
+
respondToTask(taskId: string, approved: boolean): boolean;
|
|
136
141
|
/** Resolve a pending A2A callback from sub-agent. */
|
|
137
142
|
resolveA2ACallback(taskId: string, result: string): boolean;
|
|
138
143
|
addNotificationTarget(key: string, target: NotificationTarget): void;
|
|
139
144
|
/** Consistent name for this agent: AgentCard.name or fallback. */
|
|
140
145
|
private getFormattedName;
|
|
141
146
|
/** Send a notification to all known targets with detailed logging. */
|
|
147
|
+
/** Discover the most recently active non-internal session via sessions_list. */
|
|
148
|
+
private discoverActiveSession;
|
|
142
149
|
notifyUser(message: string): Promise<void>;
|
|
143
150
|
private log;
|
|
144
151
|
}
|
|
@@ -15,6 +15,7 @@ const express_1 = __importDefault(require("express"));
|
|
|
15
15
|
const server_1 = require("@a2a-js/sdk/server");
|
|
16
16
|
const express_2 = require("@a2a-js/sdk/server/express");
|
|
17
17
|
const client_1 = require("@a2a-js/sdk/client");
|
|
18
|
+
const version_1 = require("../infra/version");
|
|
18
19
|
const a2a_adapter_1 = require("./a2a-adapter");
|
|
19
20
|
const agent_registry_1 = require("./agent-registry");
|
|
20
21
|
const agent_profile_1 = require("./agent-profile");
|
|
@@ -30,22 +31,22 @@ function buildDelegationPrompt(agent, task) {
|
|
|
30
31
|
const bioSnippet = agent.description
|
|
31
32
|
? `\n**智能体能力**: ${agent.description.slice(0, 500)}`
|
|
32
33
|
: "";
|
|
33
|
-
return `## 委派任务
|
|
34
|
-
向远端智能体发送任务并汇报结果。
|
|
35
|
-
|
|
36
|
-
**目标智能体**: ${agent.name} (${agent.url})${bioSnippet}
|
|
37
|
-
**任务内容**: ${task}
|
|
38
|
-
|
|
39
|
-
## 执行步骤
|
|
40
|
-
1. 调用 multiclaws_delegate_send(agentUrl="${agent.url}", task="${task.replace(/"/g, '\\"')}") 发送任务
|
|
41
|
-
2. 收到回复后,调用 multiclaws_notify(message="结果内容") 将结果推送给用户
|
|
42
|
-
3. 如果需要进一步沟通,可再次调用 multiclaws_delegate_send(最多 5 轮)
|
|
43
|
-
4. 每次收到回复后立即调用 multiclaws_notify 推送进展
|
|
44
|
-
|
|
45
|
-
## 规则
|
|
46
|
-
- 使用 multiclaws_delegate_send(不是 multiclaws_delegate)发送任务
|
|
47
|
-
- 使用 multiclaws_notify(不是 message)将结果推送给用户
|
|
48
|
-
- 最多 5 轮沟通
|
|
34
|
+
return `## 委派任务
|
|
35
|
+
向远端智能体发送任务并汇报结果。
|
|
36
|
+
|
|
37
|
+
**目标智能体**: ${agent.name} (${agent.url})${bioSnippet}
|
|
38
|
+
**任务内容**: ${task}
|
|
39
|
+
|
|
40
|
+
## 执行步骤
|
|
41
|
+
1. 调用 multiclaws_delegate_send(agentUrl="${agent.url}", task="${task.replace(/"/g, '\\"')}") 发送任务
|
|
42
|
+
2. 收到回复后,调用 multiclaws_notify(message="结果内容") 将结果推送给用户
|
|
43
|
+
3. 如果需要进一步沟通,可再次调用 multiclaws_delegate_send(最多 5 轮)
|
|
44
|
+
4. 每次收到回复后立即调用 multiclaws_notify 推送进展
|
|
45
|
+
|
|
46
|
+
## 规则
|
|
47
|
+
- 使用 multiclaws_delegate_send(不是 multiclaws_delegate)发送任务
|
|
48
|
+
- 使用 multiclaws_notify(不是 message)将结果推送给用户
|
|
49
|
+
- 最多 5 轮沟通
|
|
49
50
|
- 遇到错误时在 multiclaws_notify 中说明失败原因`;
|
|
50
51
|
}
|
|
51
52
|
/* ------------------------------------------------------------------ */
|
|
@@ -120,13 +121,16 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
120
121
|
taskTracker: this.taskTracker,
|
|
121
122
|
cwd: this.resolvedCwd,
|
|
122
123
|
getNotificationTargets: () => this.notificationTargets,
|
|
124
|
+
registerDiscoveredTarget: (sessionKey) => {
|
|
125
|
+
this.addNotificationTarget(`web:${sessionKey}`, { type: "web", sessionKey });
|
|
126
|
+
},
|
|
123
127
|
logger,
|
|
124
128
|
});
|
|
125
129
|
this.agentCard = {
|
|
126
130
|
name: profile.ownerName?.trim() ? (0, agent_profile_1.formatAgentCardName)(profile.ownerName.trim()) : "OpenClaw Agent",
|
|
127
131
|
description: this.profileDescription,
|
|
128
132
|
url: this.selfUrl,
|
|
129
|
-
version:
|
|
133
|
+
version: version_1.PLUGIN_VERSION,
|
|
130
134
|
protocolVersion: "0.2.2",
|
|
131
135
|
defaultInputModes: ["text/plain"],
|
|
132
136
|
defaultOutputModes: ["text/plain"],
|
|
@@ -940,6 +944,20 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
940
944
|
}
|
|
941
945
|
throw lastError;
|
|
942
946
|
}
|
|
947
|
+
/**
|
|
948
|
+
* Called by the `multiclaws_task_respond` tool when the local human
|
|
949
|
+
* approves or rejects a pending risky incoming task.
|
|
950
|
+
*/
|
|
951
|
+
respondToTask(taskId, approved) {
|
|
952
|
+
this.log("info", `respondToTask(taskId=${taskId}, approved=${approved})`);
|
|
953
|
+
if (!this.agentExecutor) {
|
|
954
|
+
this.log("warn", `respondToTask: no agentExecutor available for taskId=${taskId}`);
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
const resolved = this.agentExecutor.resolveApproval(taskId, approved);
|
|
958
|
+
this.log("info", `respondToTask: ${resolved ? "✓" : "✗"} taskId=${taskId} ${resolved ? "resolved" : "no pending approval found"}`);
|
|
959
|
+
return resolved;
|
|
960
|
+
}
|
|
943
961
|
/** Resolve a pending A2A callback from sub-agent. */
|
|
944
962
|
resolveA2ACallback(taskId, result) {
|
|
945
963
|
this.log("info", `[a2a-callback] resolveA2ACallback(taskId=${taskId}, resultLen=${result.length})`);
|
|
@@ -962,10 +980,54 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
962
980
|
return this.agentCard?.name ?? "OpenClaw Agent";
|
|
963
981
|
}
|
|
964
982
|
/** Send a notification to all known targets with detailed logging. */
|
|
983
|
+
/** Discover the most recently active non-internal session via sessions_list. */
|
|
984
|
+
async discoverActiveSession() {
|
|
985
|
+
if (!this.gatewayConfig)
|
|
986
|
+
return null;
|
|
987
|
+
try {
|
|
988
|
+
const result = await (0, gateway_client_1.invokeGatewayTool)({
|
|
989
|
+
gateway: this.gatewayConfig,
|
|
990
|
+
tool: "sessions_list",
|
|
991
|
+
args: { limit: 10, activeMinutes: 120 },
|
|
992
|
+
timeoutMs: 5_000,
|
|
993
|
+
});
|
|
994
|
+
const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
|
|
995
|
+
const session = result?.sessions?.find((s) => s.sessionKey && !INTERNAL_PREFIXES.some((p) => s.sessionKey.startsWith(p)));
|
|
996
|
+
return session?.sessionKey ?? null;
|
|
997
|
+
}
|
|
998
|
+
catch (err) {
|
|
999
|
+
this.log("warn", `discoverActiveSession failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
965
1003
|
async notifyUser(message) {
|
|
966
1004
|
this.log("info", `notifyUser: targets=${this.notificationTargets.size}, msg=${message.slice(0, 80)}`);
|
|
967
|
-
if (!this.gatewayConfig
|
|
968
|
-
this.log("warn", "notifyUser: skipped — no gatewayConfig
|
|
1005
|
+
if (!this.gatewayConfig) {
|
|
1006
|
+
this.log("warn", "notifyUser: skipped — no gatewayConfig");
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
// Fallback: no registered targets yet (e.g. right after gateway restart)
|
|
1010
|
+
if (this.notificationTargets.size === 0) {
|
|
1011
|
+
this.log("warn", "notifyUser: no registered targets — attempting session discovery");
|
|
1012
|
+
const sessionKey = await this.discoverActiveSession();
|
|
1013
|
+
if (sessionKey) {
|
|
1014
|
+
this.log("info", `notifyUser: discovered session ${sessionKey}`);
|
|
1015
|
+
try {
|
|
1016
|
+
await (0, gateway_client_1.invokeGatewayTool)({
|
|
1017
|
+
gateway: this.gatewayConfig,
|
|
1018
|
+
tool: "sessions_send",
|
|
1019
|
+
args: { sessionKey, message },
|
|
1020
|
+
timeoutMs: 5_000,
|
|
1021
|
+
});
|
|
1022
|
+
this.addNotificationTarget(`web:${sessionKey}`, { type: "web", sessionKey });
|
|
1023
|
+
}
|
|
1024
|
+
catch (err) {
|
|
1025
|
+
this.log("warn", `notifyUser: sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
else {
|
|
1029
|
+
this.log("warn", "notifyUser: no active session found, message lost");
|
|
1030
|
+
}
|
|
969
1031
|
return;
|
|
970
1032
|
}
|
|
971
1033
|
const entries = [...this.notificationTargets.entries()];
|
|
@@ -980,8 +1042,10 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
980
1042
|
timeoutMs: 5_000,
|
|
981
1043
|
})
|
|
982
1044
|
: (0, gateway_client_1.invokeGatewayTool)({
|
|
1045
|
+
// sessions_send injects a message into the session so the AI
|
|
1046
|
+
// can relay it to the human (correct tool; was "chat.send" before)
|
|
983
1047
|
gateway: this.gatewayConfig,
|
|
984
|
-
tool: "
|
|
1048
|
+
tool: "sessions_send",
|
|
985
1049
|
args: { sessionKey: target.sessionKey, message },
|
|
986
1050
|
timeoutMs: 5_000,
|
|
987
1051
|
}));
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "multiclaws",
|
|
3
|
-
"name": "MultiClaws",
|
|
4
|
-
"description": "MultiClaws plugin for multi-instance collaboration using A2A protocol",
|
|
5
|
-
"version": "0.3.0",
|
|
6
|
-
"skills": ["./skills/multiclaws"],
|
|
7
|
-
"configSchema": {
|
|
8
|
-
"type": "object",
|
|
9
|
-
"additionalProperties": false,
|
|
10
|
-
"properties": {
|
|
11
|
-
"port": {
|
|
12
|
-
"type": "integer",
|
|
13
|
-
"minimum": 1,
|
|
14
|
-
"maximum": 65535,
|
|
15
|
-
"default": 3100
|
|
16
|
-
},
|
|
17
|
-
"displayName": {
|
|
18
|
-
"type": "string",
|
|
19
|
-
"minLength": 1
|
|
20
|
-
},
|
|
21
|
-
"selfUrl": {
|
|
22
|
-
"type": "string",
|
|
23
|
-
"description": "Publicly reachable URL for this agent. If not set, tunnel configuration is required."
|
|
24
|
-
},
|
|
25
|
-
"tunnel": {
|
|
26
|
-
"type": "object",
|
|
27
|
-
"description": "FRP tunnel configuration for cross-network connectivity. Required if selfUrl is not set.",
|
|
28
|
-
"properties": {
|
|
29
|
-
"type": { "type": "string", "enum": ["frp"] },
|
|
30
|
-
"serverAddr": { "type": "string", "description": "FRP server address (IP or domain)" },
|
|
31
|
-
"serverPort": { "type": "integer", "default": 7000, "description": "FRP server bind port" },
|
|
32
|
-
"token": { "type": "string", "description": "FRP authentication token" },
|
|
33
|
-
"portRangeStart": { "type": "integer", "description": "Start of available remote port range" },
|
|
34
|
-
"portRangeEnd": { "type": "integer", "description": "End of available remote port range (inclusive)" }
|
|
35
|
-
},
|
|
36
|
-
"required": ["type", "serverAddr", "serverPort", "token", "portRangeStart", "portRangeEnd"]
|
|
37
|
-
},
|
|
38
|
-
"telemetry": {
|
|
39
|
-
"type": "object",
|
|
40
|
-
"additionalProperties": false,
|
|
41
|
-
"properties": {
|
|
42
|
-
"consoleExporter": { "type": "boolean", "default": false }
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"id": "multiclaws",
|
|
3
|
+
"name": "MultiClaws",
|
|
4
|
+
"description": "MultiClaws plugin for multi-instance collaboration using A2A protocol",
|
|
5
|
+
"version": "0.3.0",
|
|
6
|
+
"skills": ["./skills/multiclaws", "./skills/meeting-scheduler"],
|
|
7
|
+
"configSchema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"additionalProperties": false,
|
|
10
|
+
"properties": {
|
|
11
|
+
"port": {
|
|
12
|
+
"type": "integer",
|
|
13
|
+
"minimum": 1,
|
|
14
|
+
"maximum": 65535,
|
|
15
|
+
"default": 3100
|
|
16
|
+
},
|
|
17
|
+
"displayName": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"minLength": 1
|
|
20
|
+
},
|
|
21
|
+
"selfUrl": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Publicly reachable URL for this agent. If not set, tunnel configuration is required."
|
|
24
|
+
},
|
|
25
|
+
"tunnel": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"description": "FRP tunnel configuration for cross-network connectivity. Required if selfUrl is not set.",
|
|
28
|
+
"properties": {
|
|
29
|
+
"type": { "type": "string", "enum": ["frp"] },
|
|
30
|
+
"serverAddr": { "type": "string", "description": "FRP server address (IP or domain)" },
|
|
31
|
+
"serverPort": { "type": "integer", "default": 7000, "description": "FRP server bind port" },
|
|
32
|
+
"token": { "type": "string", "description": "FRP authentication token" },
|
|
33
|
+
"portRangeStart": { "type": "integer", "description": "Start of available remote port range" },
|
|
34
|
+
"portRangeEnd": { "type": "integer", "description": "End of available remote port range (inclusive)" }
|
|
35
|
+
},
|
|
36
|
+
"required": ["type", "serverAddr", "serverPort", "token", "portRangeStart", "portRangeEnd"]
|
|
37
|
+
},
|
|
38
|
+
"telemetry": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"additionalProperties": false,
|
|
41
|
+
"properties": {
|
|
42
|
+
"consoleExporter": { "type": "boolean", "default": false }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|