multiclaws 0.4.40 → 0.4.42
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.
|
@@ -16,16 +16,16 @@ export type A2AAdapterOptions = {
|
|
|
16
16
|
};
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
|
-
* Bridges the A2A protocol to OpenClaw's
|
|
19
|
+
* Bridges the A2A protocol to OpenClaw's session injection mechanism.
|
|
20
20
|
*
|
|
21
21
|
* When a remote agent sends a task via A2A `message/send`,
|
|
22
22
|
* this executor:
|
|
23
23
|
* 1. Classifies the task risk (safe vs risky)
|
|
24
|
-
* 2.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* 4.
|
|
28
|
-
* 5. Waits for the
|
|
24
|
+
* 2. For risky tasks: pushes approval request to the user's active session and waits
|
|
25
|
+
* For safe tasks: proceeds immediately
|
|
26
|
+
* 3. Finds the target session (where user last sent a message, or main session)
|
|
27
|
+
* 4. Injects the task into that session via sessions_send — no isolated sub-session created
|
|
28
|
+
* 5. Waits for the session AI to call back via `multiclaws_a2a_callback`
|
|
29
29
|
* 6. Returns the final result as a Message
|
|
30
30
|
*/
|
|
31
31
|
export declare class OpenClawAgentExecutor implements AgentExecutor {
|
|
@@ -61,6 +61,13 @@ export declare class OpenClawAgentExecutor implements AgentExecutor {
|
|
|
61
61
|
* or rejects on timeout or cancellation.
|
|
62
62
|
*/
|
|
63
63
|
private createApprovalCallback;
|
|
64
|
+
/**
|
|
65
|
+
* Find the best target session for task injection:
|
|
66
|
+
* 1. Prefer the session where the user most recently sent a message (role === "user")
|
|
67
|
+
* 2. Fall back to the first non-internal active session (typically the main webchat session)
|
|
68
|
+
* Never returns internal sessions (delegate-*, a2a-*).
|
|
69
|
+
*/
|
|
70
|
+
private findTargetSession;
|
|
64
71
|
/**
|
|
65
72
|
* Discover the most recently active non-internal session via sessions_list.
|
|
66
73
|
* Used as fallback when no notification targets have been registered yet
|
|
@@ -58,16 +58,16 @@ function extractTextFromMessage(message) {
|
|
|
58
58
|
.join("\n");
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
* Bridges the A2A protocol to OpenClaw's
|
|
61
|
+
* Bridges the A2A protocol to OpenClaw's session injection mechanism.
|
|
62
62
|
*
|
|
63
63
|
* When a remote agent sends a task via A2A `message/send`,
|
|
64
64
|
* this executor:
|
|
65
65
|
* 1. Classifies the task risk (safe vs risky)
|
|
66
|
-
* 2.
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* 4.
|
|
70
|
-
* 5. Waits for the
|
|
66
|
+
* 2. For risky tasks: pushes approval request to the user's active session and waits
|
|
67
|
+
* For safe tasks: proceeds immediately
|
|
68
|
+
* 3. Finds the target session (where user last sent a message, or main session)
|
|
69
|
+
* 4. Injects the task into that session via sessions_send — no isolated sub-session created
|
|
70
|
+
* 5. Waits for the session AI to call back via `multiclaws_a2a_callback`
|
|
71
71
|
* 6. Returns the final result as a Message
|
|
72
72
|
*/
|
|
73
73
|
class OpenClawAgentExecutor {
|
|
@@ -114,24 +114,25 @@ class OpenClawAgentExecutor {
|
|
|
114
114
|
eventBus.finished();
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
|
-
//
|
|
117
|
+
// ── Step 1: Risk classification ──
|
|
118
118
|
const risk = classifyTaskRisk(taskText);
|
|
119
|
-
this.logger.info(`[a2a-adapter] task ${taskId} risk=${risk}`);
|
|
119
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:risk-classify] risk=${risk}, text="${taskText.slice(0, 60)}"`);
|
|
120
120
|
if (risk === "risky") {
|
|
121
|
-
//
|
|
121
|
+
// ── Step 2a: Approval gate (risky tasks only) ──
|
|
122
122
|
const approvalTimeoutMs = 5 * 60 * 1000; // 5 minutes
|
|
123
123
|
const approvalPromise = this.createApprovalCallback(taskId, approvalTimeoutMs);
|
|
124
|
-
this.logger.info(`[a2a-adapter] task ${taskId}
|
|
124
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-request] sending approval request to user (timeout=${approvalTimeoutMs / 1000}s)`);
|
|
125
125
|
void this.notifyUser(buildApprovalRequest(taskId, fromAgentName, taskText));
|
|
126
126
|
let approved;
|
|
127
127
|
try {
|
|
128
128
|
approved = await approvalPromise;
|
|
129
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-result] user responded: approved=${approved}`);
|
|
129
130
|
}
|
|
130
131
|
catch (err) {
|
|
132
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
131
133
|
const isCanceled = err instanceof Error && err.message === "canceled";
|
|
132
134
|
if (isCanceled) {
|
|
133
|
-
|
|
134
|
-
this.logger.info(`[a2a-adapter] task ${taskId} canceled during approval wait`);
|
|
135
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-result] caught "canceled" error → aborting task`);
|
|
135
136
|
this.taskTracker.update(taskId, { status: "failed", error: "canceled" });
|
|
136
137
|
this.publishMessage(eventBus, "Task was canceled.");
|
|
137
138
|
eventBus.finished();
|
|
@@ -139,61 +140,66 @@ class OpenClawAgentExecutor {
|
|
|
139
140
|
}
|
|
140
141
|
// Approval timed out → auto-reject
|
|
141
142
|
approved = false;
|
|
142
|
-
this.logger.warn(`[a2a-adapter] task ${taskId} approval
|
|
143
|
+
this.logger.warn(`[a2a-adapter] task ${taskId} [step:approval-result] caught error: ${errMsg} → treating as auto-reject`);
|
|
143
144
|
}
|
|
144
145
|
if (!approved) {
|
|
145
146
|
const reason = "用户拒绝或未在超时时间内授权。";
|
|
146
|
-
this.logger.info(`[a2a-adapter] task ${taskId} rejected`);
|
|
147
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-rejected] → aborting task, reason: ${reason}`);
|
|
147
148
|
this.taskTracker.update(taskId, { status: "failed", error: reason });
|
|
148
149
|
this.publishMessage(eventBus, `任务已被拒绝:${reason}`);
|
|
149
150
|
eventBus.finished();
|
|
150
151
|
return;
|
|
151
152
|
}
|
|
152
|
-
this.logger.info(`[a2a-adapter] task ${taskId}
|
|
153
|
-
void this.notifyUser(`✅ 已授权,开始执行来自 **${fromAgentName}** 的任务…`);
|
|
153
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-passed] → proceeding to find target session`);
|
|
154
154
|
}
|
|
155
155
|
else {
|
|
156
|
-
|
|
157
|
-
this.logger.info(`[a2a-adapter] task ${taskId} safe query — auto-executing`);
|
|
158
|
-
void this.notifyUser(`📨 收到来自 **${fromAgentName}** 的查询任务(安全,自动执行):\n\n${taskText.slice(0, 300)}`);
|
|
156
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:auto-execute] safe query, skipping approval → proceeding to find target session`);
|
|
159
157
|
}
|
|
158
|
+
// ── Step 3: Find target session ──
|
|
159
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:find-session] calling findTargetSession()`);
|
|
160
|
+
const targetSessionKey = await this.findTargetSession();
|
|
161
|
+
if (!targetSessionKey) {
|
|
162
|
+
const errMsg = "无法找到用户活跃 session,任务未执行。请确保至少有一个活跃的对话 session。";
|
|
163
|
+
this.logger.error(`[a2a-adapter] task ${taskId} [step:find-session] ✗ no target session found → aborting task`);
|
|
164
|
+
this.taskTracker.update(taskId, { status: "failed", error: errMsg });
|
|
165
|
+
this.publishMessage(eventBus, errMsg);
|
|
166
|
+
eventBus.finished();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:find-session] ✓ target session = ${targetSessionKey}`);
|
|
160
170
|
try {
|
|
161
|
-
//
|
|
171
|
+
// ── Step 4: Register callback ──
|
|
162
172
|
const timeoutMs = 180_000;
|
|
163
173
|
const resultPromise = this.createCallback(taskId, timeoutMs);
|
|
164
|
-
this.logger.info(`[a2a-adapter] task ${taskId} callback registered (timeout=${timeoutMs / 1000}s)`);
|
|
165
|
-
//
|
|
166
|
-
const prompt =
|
|
167
|
-
this.logger.info(`[a2a-adapter] task ${taskId}
|
|
168
|
-
|
|
174
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:register-callback] callback registered (timeout=${timeoutMs / 1000}s, pending total=${this.pendingCallbacks.size})`);
|
|
175
|
+
// ── Step 5: Inject task into target session ──
|
|
176
|
+
const prompt = buildA2AMainSessionPrompt(taskId, fromAgentName, taskText);
|
|
177
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:inject-task] calling sessions_send(sessionKey=${targetSessionKey}, promptLen=${prompt.length})`);
|
|
178
|
+
await (0, gateway_client_1.invokeGatewayTool)({
|
|
169
179
|
gateway: this.gatewayConfig,
|
|
170
|
-
tool: "
|
|
171
|
-
args: {
|
|
172
|
-
task: prompt,
|
|
173
|
-
mode: "run",
|
|
174
|
-
cwd: this.cwd,
|
|
175
|
-
},
|
|
176
|
-
sessionKey: `a2a-${taskId}`,
|
|
180
|
+
tool: "sessions_send",
|
|
181
|
+
args: { sessionKey: targetSessionKey, message: prompt },
|
|
177
182
|
timeoutMs: 15_000,
|
|
178
183
|
});
|
|
179
|
-
this.logger.info(`[a2a-adapter] task ${taskId}
|
|
180
|
-
|
|
181
|
-
// Wait for the sub-agent to call back
|
|
184
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:inject-task] ✓ sessions_send succeeded → waiting for callback...`);
|
|
185
|
+
// ── Step 6: Wait for callback ──
|
|
182
186
|
const output = await resultPromise;
|
|
183
|
-
// Return result
|
|
187
|
+
// ── Step 7: Return result ──
|
|
184
188
|
this.taskTracker.update(taskId, { status: "completed", result: output });
|
|
185
|
-
this.logger.info(`[a2a-adapter]
|
|
186
|
-
void this.notifyUser(`✅ **来自 ${fromAgentName} 的任务已完成**\n\n${output.slice(0, 800)}`);
|
|
189
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:completed] ✓ resultLen=${output.length}, preview="${output.slice(0, 120)}"`);
|
|
187
190
|
this.publishMessage(eventBus, output || "Task completed with no output.");
|
|
188
191
|
}
|
|
189
192
|
catch (err) {
|
|
190
193
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
191
|
-
|
|
194
|
+
const isCanceled = err instanceof Error && err.message === "canceled";
|
|
195
|
+
const isTimeout = errorMsg.includes("timed out");
|
|
196
|
+
const errorType = isCanceled ? "canceled" : isTimeout ? "timeout" : "error";
|
|
197
|
+
this.logger.error(`[a2a-adapter] task ${taskId} [step:catch] ✗ type=${errorType}, reason: ${errorMsg} → marking failed, notifying user`);
|
|
192
198
|
this.taskTracker.update(taskId, { status: "failed", error: errorMsg });
|
|
193
199
|
void this.notifyUser(`❌ 来自 **${fromAgentName}** 的任务执行失败:${errorMsg}`);
|
|
194
200
|
this.publishMessage(eventBus, `Error: ${errorMsg}`);
|
|
195
201
|
}
|
|
196
|
-
this.logger.info(`[a2a-adapter] task ${taskId} eventBus.finished()`);
|
|
202
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:finished] eventBus.finished()`);
|
|
197
203
|
eventBus.finished();
|
|
198
204
|
}
|
|
199
205
|
/**
|
|
@@ -281,15 +287,81 @@ class OpenClawAgentExecutor {
|
|
|
281
287
|
this.pendingApprovals.set(taskId, { resolve, reject, timer });
|
|
282
288
|
});
|
|
283
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Find the best target session for task injection:
|
|
292
|
+
* 1. Prefer the session where the user most recently sent a message (role === "user")
|
|
293
|
+
* 2. Fall back to the first non-internal active session (typically the main webchat session)
|
|
294
|
+
* Never returns internal sessions (delegate-*, a2a-*).
|
|
295
|
+
*/
|
|
296
|
+
async findTargetSession() {
|
|
297
|
+
if (!this.gatewayConfig) {
|
|
298
|
+
this.logger.warn(`[a2a-adapter] findTargetSession: skipped — no gateway config`);
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
this.logger.info(`[a2a-adapter] findTargetSession: calling sessions_list (limit=20, activeMinutes=1440)`);
|
|
303
|
+
const raw = await (0, gateway_client_1.invokeGatewayTool)({
|
|
304
|
+
gateway: this.gatewayConfig,
|
|
305
|
+
tool: "sessions_list",
|
|
306
|
+
args: { limit: 20, activeMinutes: 1440, messageLimit: 3 },
|
|
307
|
+
timeoutMs: 5_000,
|
|
308
|
+
});
|
|
309
|
+
this.logger.info(`[a2a-adapter] findTargetSession: raw result = ${JSON.stringify(raw).slice(0, 500)}`);
|
|
310
|
+
// Unwrap gateway tool standard response: { content: [{ type: "text", text: "..." }] }
|
|
311
|
+
let parsed = raw;
|
|
312
|
+
if (raw?.content?.[0]?.type === "text") {
|
|
313
|
+
try {
|
|
314
|
+
parsed = JSON.parse(raw.content[0].text);
|
|
315
|
+
this.logger.info(`[a2a-adapter] findTargetSession: unwrapped gateway response successfully`);
|
|
316
|
+
}
|
|
317
|
+
catch (parseErr) {
|
|
318
|
+
this.logger.warn(`[a2a-adapter] findTargetSession: failed to parse content[0].text as JSON — ${parseErr instanceof Error ? parseErr.message : String(parseErr)}, using raw object`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
|
|
322
|
+
const sessions = parsed?.sessions ?? [];
|
|
323
|
+
this.logger.info(`[a2a-adapter] findTargetSession: ${sessions.length} total sessions from gateway`);
|
|
324
|
+
const filtered = sessions.filter((s) => {
|
|
325
|
+
const k = (s.key ?? s.sessionKey);
|
|
326
|
+
return k && !INTERNAL_PREFIXES.some((p) => k.startsWith(p));
|
|
327
|
+
});
|
|
328
|
+
this.logger.info(`[a2a-adapter] findTargetSession: ${filtered.length} non-internal sessions after filtering`);
|
|
329
|
+
// Prefer sessions that have at least one user-originated message
|
|
330
|
+
const withUserMsg = filtered.filter((s) => Array.isArray(s.messages) && s.messages.some((m) => m.role === "user"));
|
|
331
|
+
// Fall back to any non-internal session (likely the main webchat session)
|
|
332
|
+
const target = withUserMsg[0] ?? filtered[0];
|
|
333
|
+
const targetKey = (target?.key ?? target?.sessionKey);
|
|
334
|
+
if (targetKey) {
|
|
335
|
+
const source = withUserMsg.length > 0 ? "user-message session" : "fallback non-internal session";
|
|
336
|
+
this.logger.info(`[a2a-adapter] findTargetSession: ✓ matched ${targetKey} (${source}, ${withUserMsg.length} with user msgs, ${filtered.length} total)`);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
this.logger.warn(`[a2a-adapter] findTargetSession: ✗ no target found (${sessions.length} raw, ${filtered.length} after filter, ${withUserMsg.length} with user msgs)`);
|
|
340
|
+
sessions.forEach((s, i) => {
|
|
341
|
+
const k = (s.key ?? s.sessionKey) ?? "(no key)";
|
|
342
|
+
const msgCount = Array.isArray(s.messages) ? s.messages.length : 0;
|
|
343
|
+
this.logger.info(`[a2a-adapter] findTargetSession: session[${i}]: key=${k}, messages=${msgCount}`);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return targetKey ?? null;
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
this.logger.error(`[a2a-adapter] findTargetSession: ✗ caught error — ${err instanceof Error ? err.message : String(err)}, returning null`);
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
284
353
|
/**
|
|
285
354
|
* Discover the most recently active non-internal session via sessions_list.
|
|
286
355
|
* Used as fallback when no notification targets have been registered yet
|
|
287
356
|
* (e.g. right after a gateway restart before the user sends their first message).
|
|
288
357
|
*/
|
|
289
358
|
async discoverActiveSession() {
|
|
290
|
-
if (!this.gatewayConfig)
|
|
359
|
+
if (!this.gatewayConfig) {
|
|
360
|
+
this.logger.warn(`[a2a-adapter] discoverActiveSession: skipped — no gateway config`);
|
|
291
361
|
return null;
|
|
362
|
+
}
|
|
292
363
|
try {
|
|
364
|
+
this.logger.info(`[a2a-adapter] discoverActiveSession: calling sessions_list (limit=10, activeMinutes=120)`);
|
|
293
365
|
const raw = await (0, gateway_client_1.invokeGatewayTool)({
|
|
294
366
|
gateway: this.gatewayConfig,
|
|
295
367
|
tool: "sessions_list",
|
|
@@ -302,8 +374,11 @@ class OpenClawAgentExecutor {
|
|
|
302
374
|
if (raw?.content?.[0]?.type === "text") {
|
|
303
375
|
try {
|
|
304
376
|
parsed = JSON.parse(raw.content[0].text);
|
|
377
|
+
this.logger.info(`[a2a-adapter] discoverActiveSession: unwrapped gateway response successfully`);
|
|
378
|
+
}
|
|
379
|
+
catch (parseErr) {
|
|
380
|
+
this.logger.warn(`[a2a-adapter] discoverActiveSession: failed to parse content[0].text as JSON — ${parseErr instanceof Error ? parseErr.message : String(parseErr)}, using raw object`);
|
|
305
381
|
}
|
|
306
|
-
catch { /* use raw */ }
|
|
307
382
|
}
|
|
308
383
|
const sessions = parsed?.sessions ?? [];
|
|
309
384
|
this.logger.info(`[a2a-adapter] discoverActiveSession: found ${sessions.length} sessions`);
|
|
@@ -315,33 +390,34 @@ class OpenClawAgentExecutor {
|
|
|
315
390
|
});
|
|
316
391
|
const matchedKey = (session?.key ?? session?.sessionKey);
|
|
317
392
|
if (matchedKey) {
|
|
318
|
-
this.logger.info(`[a2a-adapter] discoverActiveSession: matched session ${matchedKey}`);
|
|
393
|
+
this.logger.info(`[a2a-adapter] discoverActiveSession: ✓ matched session ${matchedKey}`);
|
|
319
394
|
}
|
|
320
395
|
else {
|
|
321
|
-
this.logger.warn(`[a2a-adapter] discoverActiveSession: all ${sessions.length} sessions filtered or empty`);
|
|
322
|
-
sessions.forEach((s) => this.logger.info(`[a2a-adapter] session:
|
|
396
|
+
this.logger.warn(`[a2a-adapter] discoverActiveSession: ✗ all ${sessions.length} sessions filtered or empty`);
|
|
397
|
+
sessions.forEach((s, i) => this.logger.info(`[a2a-adapter] discoverActiveSession: session[${i}]: key=${(s.key ?? s.sessionKey) ?? "(no key)"}`));
|
|
323
398
|
}
|
|
324
399
|
return matchedKey ?? null;
|
|
325
400
|
}
|
|
326
401
|
catch (err) {
|
|
327
|
-
this.logger.
|
|
402
|
+
this.logger.error(`[a2a-adapter] discoverActiveSession: ✗ caught error — ${err instanceof Error ? err.message : String(err)}, returning null`);
|
|
328
403
|
return null;
|
|
329
404
|
}
|
|
330
405
|
}
|
|
331
406
|
/** Send a notification to all known targets. Individual failures are silently ignored. */
|
|
332
407
|
async notifyUser(message) {
|
|
333
408
|
const targets = this.getNotificationTargets();
|
|
409
|
+
this.logger.info(`[a2a-adapter] notifyUser: targets=${targets.size}, msgLen=${message.length}, preview="${message.slice(0, 80)}"`);
|
|
334
410
|
if (!this.gatewayConfig) {
|
|
335
|
-
this.logger.
|
|
411
|
+
this.logger.warn(`[a2a-adapter] notifyUser: skipped — no gateway config, message lost`);
|
|
336
412
|
return;
|
|
337
413
|
}
|
|
338
414
|
// Fallback: no registered targets yet (e.g. right after gateway restart).
|
|
339
415
|
// Discover the active session and send directly via sessions_send.
|
|
340
416
|
if (targets.size === 0) {
|
|
341
|
-
this.logger.info(`[a2a-adapter] notifyUser: no registered targets
|
|
342
|
-
const sessionKey = await this.
|
|
417
|
+
this.logger.info(`[a2a-adapter] notifyUser: no registered targets → falling back to findTargetSession()`);
|
|
418
|
+
const sessionKey = await this.findTargetSession();
|
|
343
419
|
if (sessionKey) {
|
|
344
|
-
this.logger.info(`[a2a-adapter] notifyUser: discovered session ${sessionKey}
|
|
420
|
+
this.logger.info(`[a2a-adapter] notifyUser: fallback discovered session ${sessionKey} → calling sessions_send`);
|
|
345
421
|
try {
|
|
346
422
|
await (0, gateway_client_1.invokeGatewayTool)({
|
|
347
423
|
gateway: this.gatewayConfig,
|
|
@@ -349,22 +425,25 @@ class OpenClawAgentExecutor {
|
|
|
349
425
|
args: { sessionKey, message },
|
|
350
426
|
timeoutMs: 5_000,
|
|
351
427
|
});
|
|
428
|
+
this.logger.info(`[a2a-adapter] notifyUser: ✓ fallback sessions_send to ${sessionKey} succeeded`);
|
|
352
429
|
// Also register this session for future notifications
|
|
353
430
|
if (this.registerDiscoveredTarget) {
|
|
354
431
|
this.registerDiscoveredTarget(sessionKey);
|
|
432
|
+
this.logger.info(`[a2a-adapter] notifyUser: registered ${sessionKey} as notification target for future use`);
|
|
355
433
|
}
|
|
356
434
|
}
|
|
357
435
|
catch (err) {
|
|
358
|
-
this.logger.
|
|
436
|
+
this.logger.error(`[a2a-adapter] notifyUser: ✗ fallback sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
359
437
|
}
|
|
360
438
|
}
|
|
361
439
|
else {
|
|
362
|
-
this.logger.warn(`[a2a-adapter] notifyUser: no active session found, message lost`);
|
|
440
|
+
this.logger.warn(`[a2a-adapter] notifyUser: ✗ findTargetSession returned null — no active session found, message lost`);
|
|
363
441
|
}
|
|
364
442
|
return;
|
|
365
443
|
}
|
|
444
|
+
this.logger.info(`[a2a-adapter] notifyUser: sending to ${targets.size} registered target(s): [${[...targets.keys()].join(", ")}]`);
|
|
366
445
|
const results = await Promise.allSettled([...targets.entries()].map(async ([key, target]) => {
|
|
367
|
-
this.logger.info(`[a2a-adapter] notifyUser:
|
|
446
|
+
this.logger.info(`[a2a-adapter] notifyUser: → ${key} (type=${target.type})`);
|
|
368
447
|
try {
|
|
369
448
|
await (target.type === "channel"
|
|
370
449
|
? (0, gateway_client_1.invokeGatewayTool)({
|
|
@@ -374,23 +453,26 @@ class OpenClawAgentExecutor {
|
|
|
374
453
|
timeoutMs: 5_000,
|
|
375
454
|
})
|
|
376
455
|
: (0, gateway_client_1.invokeGatewayTool)({
|
|
377
|
-
// sessions_send injects a message into the session so the AI
|
|
378
|
-
// can relay it to the human (correct tool; was "chat.send" before)
|
|
379
456
|
gateway: this.gatewayConfig,
|
|
380
457
|
tool: "sessions_send",
|
|
381
458
|
args: { sessionKey: target.sessionKey, message },
|
|
382
459
|
timeoutMs: 5_000,
|
|
383
460
|
}));
|
|
384
|
-
this.logger.info(`[a2a-adapter] notifyUser:
|
|
461
|
+
this.logger.info(`[a2a-adapter] notifyUser: ✓ ${key} (${target.type}) succeeded`);
|
|
385
462
|
}
|
|
386
463
|
catch (err) {
|
|
387
|
-
this.logger.
|
|
464
|
+
this.logger.error(`[a2a-adapter] notifyUser: ✗ ${key} (${target.type}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
388
465
|
throw err;
|
|
389
466
|
}
|
|
390
467
|
}));
|
|
391
468
|
const ok = results.filter((r) => r.status === "fulfilled").length;
|
|
392
469
|
const fail = results.filter((r) => r.status === "rejected").length;
|
|
393
|
-
|
|
470
|
+
if (fail === 0) {
|
|
471
|
+
this.logger.info(`[a2a-adapter] notifyUser: ✓ all ${ok} targets succeeded`);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
this.logger.error(`[a2a-adapter] notifyUser: done — ${ok} ok, ${fail} FAILED out of ${ok + fail} targets`);
|
|
475
|
+
}
|
|
394
476
|
}
|
|
395
477
|
publishMessage(eventBus, text) {
|
|
396
478
|
const message = {
|
|
@@ -426,27 +508,18 @@ ${preview}
|
|
|
426
508
|
授权等待时间:5 分钟,超时自动拒绝。`;
|
|
427
509
|
}
|
|
428
510
|
/**
|
|
429
|
-
* Build the prompt
|
|
430
|
-
* The
|
|
511
|
+
* Build the prompt injected into the user's active main session for an incoming A2A task.
|
|
512
|
+
* The AI in that session processes the task naturally and must call multiclaws_a2a_callback.
|
|
431
513
|
*/
|
|
432
|
-
function
|
|
433
|
-
return
|
|
434
|
-
|
|
435
|
-
## 任务内容
|
|
514
|
+
function buildA2AMainSessionPrompt(taskId, fromAgentName, taskText) {
|
|
515
|
+
return `[MultiClaws 委派任务] 来自 **${fromAgentName}**:
|
|
436
516
|
|
|
437
517
|
${taskText}
|
|
438
518
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
\`\`\`
|
|
444
|
-
multiclaws_a2a_callback(taskId="${taskId}", result="你的完整回复内容")
|
|
445
|
-
\`\`\`
|
|
519
|
+
---
|
|
520
|
+
完成后请调用 \`multiclaws_a2a_callback\` 汇报结果:
|
|
521
|
+
- taskId: "${taskId}"
|
|
522
|
+
- result: 你的完整回复内容
|
|
446
523
|
|
|
447
|
-
|
|
448
|
-
- 无论任务成功还是失败,都必须调用 \`multiclaws_a2a_callback\`
|
|
449
|
-
- result 参数填写你的完整回复文本
|
|
450
|
-
- 如果任务失败,在 result 中说明失败原因
|
|
451
|
-
- 这是唯一的结果回传方式,不调用则结果会丢失`;
|
|
524
|
+
无论成功还是失败都必须调用,这是结果回传给委派方的唯一方式。`;
|
|
452
525
|
}
|
|
@@ -143,7 +143,6 @@ export declare class MulticlawsService extends EventEmitter {
|
|
|
143
143
|
addNotificationTarget(key: string, target: NotificationTarget): void;
|
|
144
144
|
/** Consistent name for this agent: AgentCard.name or fallback. */
|
|
145
145
|
private getFormattedName;
|
|
146
|
-
/** Send a notification to all known targets with detailed logging. */
|
|
147
146
|
/** Discover the most recently active non-internal session via sessions_list. */
|
|
148
147
|
private discoverActiveSession;
|
|
149
148
|
notifyUser(message: string): Promise<void>;
|
|
@@ -256,31 +256,41 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
256
256
|
/* ---------------------------------------------------------------- */
|
|
257
257
|
async delegateTask(params) {
|
|
258
258
|
this.log("info", `[delegate] ▶ delegateTask(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
|
|
259
|
-
this.log("info", `[delegate] task preview: ${params.task.slice(0, 120)}`);
|
|
260
|
-
|
|
259
|
+
this.log("info", `[delegate] task preview: "${params.task.slice(0, 120)}"`);
|
|
260
|
+
// Step 1: Check profile
|
|
261
|
+
this.log("info", `[delegate] [step:profile-check] verifying profile completeness`);
|
|
262
|
+
try {
|
|
263
|
+
await this.requireCompleteProfile();
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
this.log("error", `[delegate] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
|
|
267
|
+
return { status: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
268
|
+
}
|
|
269
|
+
// Step 2: Look up agent
|
|
270
|
+
this.log("info", `[delegate] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
|
|
261
271
|
const agentRecord = await this.agentRegistry.get(params.agentUrl);
|
|
262
272
|
if (!agentRecord) {
|
|
263
|
-
this.log("warn", `[delegate] ✗ unknown agent: ${params.agentUrl}`);
|
|
273
|
+
this.log("warn", `[delegate] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
|
|
264
274
|
return { status: "failed", error: `unknown agent: ${params.agentUrl}` };
|
|
265
275
|
}
|
|
266
|
-
this.log("info", `[delegate] agent found: ${agentRecord.name} (${agentRecord.url})`);
|
|
276
|
+
this.log("info", `[delegate] [step:agent-lookup] ✓ found: ${agentRecord.name} (${agentRecord.url})`);
|
|
277
|
+
// Step 3: Track task
|
|
267
278
|
const track = this.taskTracker.create({
|
|
268
279
|
fromPeerId: "local",
|
|
269
280
|
toPeerId: params.agentUrl,
|
|
270
281
|
task: params.task,
|
|
271
282
|
});
|
|
272
283
|
this.taskTracker.update(track.taskId, { status: "running" });
|
|
273
|
-
this.log("info", `[delegate]
|
|
284
|
+
this.log("info", `[delegate] [step:track] taskId=${track.taskId}, status=running`);
|
|
274
285
|
try {
|
|
275
|
-
|
|
286
|
+
// Step 4: Create A2A client
|
|
287
|
+
this.log("info", `[delegate] ${track.taskId} [step:create-client] creating A2A client for ${agentRecord.url}`);
|
|
276
288
|
const client = await this.createA2AClient(agentRecord);
|
|
277
|
-
this.log("info", `[delegate] ${track.taskId}
|
|
278
|
-
// Fire-and-forget execution
|
|
279
|
-
// the gateway call can return quickly and the task can outlive
|
|
280
|
-
// the gateway's HTTP timeout.
|
|
289
|
+
this.log("info", `[delegate] ${track.taskId} [step:create-client] ✓ client created → starting fire-and-forget send`);
|
|
290
|
+
// Step 5: Fire-and-forget execution
|
|
281
291
|
void (async () => {
|
|
282
292
|
try {
|
|
283
|
-
this.log("info", `[delegate] ${track.taskId} sending A2A message
|
|
293
|
+
this.log("info", `[delegate] ${track.taskId} [step:background-send] sending A2A message to ${agentRecord.name}...`);
|
|
284
294
|
const result = await client.sendMessage({
|
|
285
295
|
message: {
|
|
286
296
|
kind: "message",
|
|
@@ -289,24 +299,22 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
289
299
|
messageId: track.taskId,
|
|
290
300
|
},
|
|
291
301
|
});
|
|
292
|
-
this.log("info", `[delegate] ${track.taskId} A2A response received
|
|
302
|
+
this.log("info", `[delegate] ${track.taskId} [step:background-send] ✓ A2A response received → processing result`);
|
|
293
303
|
this.processTaskResult(track.taskId, result);
|
|
294
304
|
}
|
|
295
305
|
catch (err) {
|
|
296
306
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
297
307
|
this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
|
|
298
|
-
this.log("error", `[delegate]
|
|
308
|
+
this.log("error", `[delegate] ${track.taskId} [step:background-send] ✗ caught error: ${errorMsg} → task marked failed`);
|
|
299
309
|
}
|
|
300
310
|
})();
|
|
301
|
-
|
|
302
|
-
// do not depend on the remote agent's total execution time.
|
|
303
|
-
this.log("info", `[delegate] ${track.taskId} returned immediately (fire-and-forget)`);
|
|
311
|
+
this.log("info", `[delegate] ${track.taskId} [step:return] returned immediately (fire-and-forget), background send in progress`);
|
|
304
312
|
return { taskId: track.taskId, status: "running" };
|
|
305
313
|
}
|
|
306
314
|
catch (err) {
|
|
307
315
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
308
316
|
this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
|
|
309
|
-
this.log("error", `[delegate]
|
|
317
|
+
this.log("error", `[delegate] ${track.taskId} [step:catch] ✗ caught error during client creation: ${errorMsg} → task marked failed`);
|
|
310
318
|
return { taskId: track.taskId, status: "failed", error: errorMsg };
|
|
311
319
|
}
|
|
312
320
|
}
|
|
@@ -316,25 +324,38 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
316
324
|
*/
|
|
317
325
|
async delegateTaskSync(params) {
|
|
318
326
|
this.log("info", `[delegate-sync] ▶ delegateTaskSync(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
|
|
319
|
-
this.log("info", `[delegate-sync] task preview: ${params.task.slice(0, 120)}`);
|
|
320
|
-
|
|
327
|
+
this.log("info", `[delegate-sync] task preview: "${params.task.slice(0, 120)}"`);
|
|
328
|
+
// Step 1: Check profile
|
|
329
|
+
this.log("info", `[delegate-sync] [step:profile-check] verifying profile completeness`);
|
|
330
|
+
try {
|
|
331
|
+
await this.requireCompleteProfile();
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
this.log("error", `[delegate-sync] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
|
|
335
|
+
return { status: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
336
|
+
}
|
|
337
|
+
// Step 2: Look up agent
|
|
338
|
+
this.log("info", `[delegate-sync] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
|
|
321
339
|
const agentRecord = await this.agentRegistry.get(params.agentUrl);
|
|
322
340
|
if (!agentRecord) {
|
|
323
|
-
this.log("warn", `[delegate-sync] ✗ unknown agent: ${params.agentUrl}`);
|
|
341
|
+
this.log("warn", `[delegate-sync] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
|
|
324
342
|
return { status: "failed", error: `unknown agent: ${params.agentUrl}` };
|
|
325
343
|
}
|
|
326
|
-
this.log("info", `[delegate-sync] agent found: ${agentRecord.name} (${agentRecord.url})`);
|
|
344
|
+
this.log("info", `[delegate-sync] [step:agent-lookup] ✓ found: ${agentRecord.name} (${agentRecord.url})`);
|
|
345
|
+
// Step 3: Track task
|
|
327
346
|
const track = this.taskTracker.create({
|
|
328
347
|
fromPeerId: "local",
|
|
329
348
|
toPeerId: params.agentUrl,
|
|
330
349
|
task: params.task,
|
|
331
350
|
});
|
|
332
351
|
this.taskTracker.update(track.taskId, { status: "running" });
|
|
333
|
-
this.log("info", `[delegate-sync]
|
|
352
|
+
this.log("info", `[delegate-sync] [step:track] taskId=${track.taskId}, status=running`);
|
|
334
353
|
try {
|
|
335
|
-
|
|
354
|
+
// Step 4: Create A2A client
|
|
355
|
+
this.log("info", `[delegate-sync] ${track.taskId} [step:create-client] creating A2A client for ${agentRecord.url}`);
|
|
336
356
|
const client = await this.createA2AClient(agentRecord);
|
|
337
|
-
|
|
357
|
+
// Step 5: Send A2A message (synchronous — blocks until response)
|
|
358
|
+
this.log("info", `[delegate-sync] ${track.taskId} [step:send] sending A2A message (sync, metadata: selfUrl=${this.selfUrl}, selfName=${this.agentCard?.name ?? "unknown"})...`);
|
|
338
359
|
const result = await client.sendMessage({
|
|
339
360
|
message: {
|
|
340
361
|
kind: "message",
|
|
@@ -347,15 +368,16 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
347
368
|
},
|
|
348
369
|
},
|
|
349
370
|
});
|
|
350
|
-
this.log("info", `[delegate-sync] ${track.taskId} A2A response received`);
|
|
371
|
+
this.log("info", `[delegate-sync] ${track.taskId} [step:send] ✓ A2A response received → processing result`);
|
|
372
|
+
// Step 6: Process result
|
|
351
373
|
const taskResult = this.processTaskResult(track.taskId, result);
|
|
352
|
-
this.log("info", `[delegate-sync]
|
|
374
|
+
this.log("info", `[delegate-sync] ${track.taskId} [step:completed] ✓ status=${taskResult.status}, outputLen=${taskResult.output?.length ?? 0}, preview="${(taskResult.output ?? "").slice(0, 120)}"`);
|
|
353
375
|
return taskResult;
|
|
354
376
|
}
|
|
355
377
|
catch (err) {
|
|
356
378
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
357
379
|
this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
|
|
358
|
-
this.log("error", `[delegate-sync]
|
|
380
|
+
this.log("error", `[delegate-sync] ${track.taskId} [step:catch] ✗ caught error: ${errorMsg} → task marked failed`);
|
|
359
381
|
return { taskId: track.taskId, status: "failed", error: errorMsg };
|
|
360
382
|
}
|
|
361
383
|
}
|
|
@@ -366,30 +388,48 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
366
388
|
*/
|
|
367
389
|
async spawnDelegation(params) {
|
|
368
390
|
this.log("info", `[spawn-delegate] ▶ spawnDelegation(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
|
|
369
|
-
this.log("info", `[spawn-delegate] task preview: ${params.task.slice(0, 120)}`);
|
|
370
|
-
|
|
391
|
+
this.log("info", `[spawn-delegate] task preview: "${params.task.slice(0, 120)}"`);
|
|
392
|
+
// Step 1: Check profile
|
|
393
|
+
this.log("info", `[spawn-delegate] [step:profile-check] verifying profile completeness`);
|
|
394
|
+
try {
|
|
395
|
+
await this.requireCompleteProfile();
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
this.log("error", `[spawn-delegate] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
|
|
399
|
+
throw err;
|
|
400
|
+
}
|
|
401
|
+
// Step 2: Look up agent
|
|
402
|
+
this.log("info", `[spawn-delegate] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
|
|
371
403
|
const agent = await this.agentRegistry.get(params.agentUrl);
|
|
372
404
|
if (!agent) {
|
|
373
|
-
this.log("warn", `[spawn-delegate] ✗ unknown agent: ${params.agentUrl}`);
|
|
405
|
+
this.log("warn", `[spawn-delegate] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
|
|
374
406
|
throw new Error(`unknown agent: ${params.agentUrl}`);
|
|
375
407
|
}
|
|
376
|
-
this.log("info", `[spawn-delegate] agent found: ${agent.name} (${agent.url})`);
|
|
408
|
+
this.log("info", `[spawn-delegate] [step:agent-lookup] ✓ found: ${agent.name} (${agent.url})`);
|
|
409
|
+
// Step 3: Check gateway config
|
|
377
410
|
if (!this.gatewayConfig) {
|
|
378
|
-
this.log("error", `[spawn-delegate] ✗ gateway config not available`);
|
|
411
|
+
this.log("error", `[spawn-delegate] [step:gateway-check] ✗ gateway config not available → aborting`);
|
|
379
412
|
throw new Error("gateway config not available — cannot spawn sub-agent");
|
|
380
413
|
}
|
|
414
|
+
// Step 4: Spawn sub-agent
|
|
381
415
|
const prompt = buildDelegationPrompt(agent, params.task);
|
|
382
416
|
const sessionKey = `delegate-${Date.now()}`;
|
|
383
|
-
this.log("info", `[spawn-delegate]
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
417
|
+
this.log("info", `[spawn-delegate] [step:spawn] calling sessions_spawn (cwd=${this.resolvedCwd}, sessionKey=${sessionKey}, promptLen=${prompt.length})`);
|
|
418
|
+
try {
|
|
419
|
+
const spawnResult = await (0, gateway_client_1.invokeGatewayTool)({
|
|
420
|
+
gateway: this.gatewayConfig,
|
|
421
|
+
tool: "sessions_spawn",
|
|
422
|
+
args: { task: prompt, mode: "run", cwd: this.resolvedCwd },
|
|
423
|
+
sessionKey,
|
|
424
|
+
timeoutMs: 15_000,
|
|
425
|
+
});
|
|
426
|
+
this.log("info", `[spawn-delegate] [step:spawn] ✓ sub-agent spawned for ${agent.name} — result=${JSON.stringify(spawnResult).slice(0, 200)}`);
|
|
427
|
+
return { message: `已启动子 agent 向 ${agent.name} 委派任务` };
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
this.log("error", `[spawn-delegate] [step:spawn] ✗ sessions_spawn failed: ${err instanceof Error ? err.message : String(err)} → aborting`);
|
|
431
|
+
throw err;
|
|
432
|
+
}
|
|
393
433
|
}
|
|
394
434
|
getTaskStatus(taskId) {
|
|
395
435
|
return this.taskTracker.get(taskId);
|
|
@@ -871,7 +911,16 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
871
911
|
}
|
|
872
912
|
}
|
|
873
913
|
async createA2AClient(agent) {
|
|
874
|
-
|
|
914
|
+
this.log("info", `[a2a-client] creating client for ${agent.name} (${agent.url})`);
|
|
915
|
+
try {
|
|
916
|
+
const client = await this.clientFactory.createFromUrl(agent.url);
|
|
917
|
+
this.log("info", `[a2a-client] ✓ client created for ${agent.name} (${agent.url})`);
|
|
918
|
+
return client;
|
|
919
|
+
}
|
|
920
|
+
catch (err) {
|
|
921
|
+
this.log("error", `[a2a-client] ✗ failed to create client for ${agent.name} (${agent.url}): ${err instanceof Error ? err.message : String(err)}`);
|
|
922
|
+
throw err;
|
|
923
|
+
}
|
|
875
924
|
}
|
|
876
925
|
/**
|
|
877
926
|
* Send a message using A2A streaming to minimize latency.
|
|
@@ -979,12 +1028,14 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
979
1028
|
getFormattedName() {
|
|
980
1029
|
return this.agentCard?.name ?? "OpenClaw Agent";
|
|
981
1030
|
}
|
|
982
|
-
/** Send a notification to all known targets with detailed logging. */
|
|
983
1031
|
/** Discover the most recently active non-internal session via sessions_list. */
|
|
984
1032
|
async discoverActiveSession() {
|
|
985
|
-
if (!this.gatewayConfig)
|
|
1033
|
+
if (!this.gatewayConfig) {
|
|
1034
|
+
this.log("warn", `discoverActiveSession: skipped — no gateway config`);
|
|
986
1035
|
return null;
|
|
1036
|
+
}
|
|
987
1037
|
try {
|
|
1038
|
+
this.log("info", `discoverActiveSession: calling sessions_list (limit=10, activeMinutes=120)`);
|
|
988
1039
|
const raw = await (0, gateway_client_1.invokeGatewayTool)({
|
|
989
1040
|
gateway: this.gatewayConfig,
|
|
990
1041
|
tool: "sessions_list",
|
|
@@ -997,11 +1048,14 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
997
1048
|
if (raw?.content?.[0]?.type === "text") {
|
|
998
1049
|
try {
|
|
999
1050
|
parsed = JSON.parse(raw.content[0].text);
|
|
1051
|
+
this.log("info", `discoverActiveSession: unwrapped gateway response successfully`);
|
|
1052
|
+
}
|
|
1053
|
+
catch (parseErr) {
|
|
1054
|
+
this.log("warn", `discoverActiveSession: failed to parse content[0].text as JSON — ${parseErr instanceof Error ? parseErr.message : String(parseErr)}, using raw object`);
|
|
1000
1055
|
}
|
|
1001
|
-
catch { /* use raw */ }
|
|
1002
1056
|
}
|
|
1003
1057
|
const sessions = parsed?.sessions ?? [];
|
|
1004
|
-
this.log("info", `discoverActiveSession: found ${sessions.length} sessions`);
|
|
1058
|
+
this.log("info", `discoverActiveSession: found ${sessions.length} sessions from gateway`);
|
|
1005
1059
|
const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
|
|
1006
1060
|
// sessions_list returns "key" not "sessionKey"
|
|
1007
1061
|
const session = sessions.find((s) => {
|
|
@@ -1010,31 +1064,31 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
1010
1064
|
});
|
|
1011
1065
|
const matchedKey = (session?.key ?? session?.sessionKey);
|
|
1012
1066
|
if (matchedKey) {
|
|
1013
|
-
this.log("info", `discoverActiveSession: matched session ${matchedKey}`);
|
|
1067
|
+
this.log("info", `discoverActiveSession: ✓ matched session ${matchedKey}`);
|
|
1014
1068
|
}
|
|
1015
1069
|
else {
|
|
1016
|
-
this.log("warn", `discoverActiveSession: all ${sessions.length} sessions filtered or empty`);
|
|
1017
|
-
sessions.forEach((s) => this.log("info", `
|
|
1070
|
+
this.log("warn", `discoverActiveSession: ✗ all ${sessions.length} sessions filtered or empty`);
|
|
1071
|
+
sessions.forEach((s, i) => this.log("info", `discoverActiveSession: session[${i}]: key=${(s.key ?? s.sessionKey) ?? "(no key)"}`));
|
|
1018
1072
|
}
|
|
1019
1073
|
return matchedKey ?? null;
|
|
1020
1074
|
}
|
|
1021
1075
|
catch (err) {
|
|
1022
|
-
this.log("
|
|
1076
|
+
this.log("error", `discoverActiveSession: ✗ caught error — ${err instanceof Error ? err.message : String(err)}, returning null`);
|
|
1023
1077
|
return null;
|
|
1024
1078
|
}
|
|
1025
1079
|
}
|
|
1026
1080
|
async notifyUser(message) {
|
|
1027
|
-
this.log("info", `notifyUser: targets=${this.notificationTargets.size},
|
|
1081
|
+
this.log("info", `notifyUser: targets=${this.notificationTargets.size}, msgLen=${message.length}, preview="${message.slice(0, 80)}"`);
|
|
1028
1082
|
if (!this.gatewayConfig) {
|
|
1029
|
-
this.log("warn", "notifyUser: skipped — no gatewayConfig");
|
|
1083
|
+
this.log("warn", "notifyUser: skipped — no gatewayConfig, message lost");
|
|
1030
1084
|
return;
|
|
1031
1085
|
}
|
|
1032
1086
|
// Fallback: no registered targets yet (e.g. right after gateway restart)
|
|
1033
1087
|
if (this.notificationTargets.size === 0) {
|
|
1034
|
-
this.log("
|
|
1088
|
+
this.log("info", "notifyUser: no registered targets → falling back to discoverActiveSession()");
|
|
1035
1089
|
const sessionKey = await this.discoverActiveSession();
|
|
1036
1090
|
if (sessionKey) {
|
|
1037
|
-
this.log("info", `notifyUser: discovered session ${sessionKey}`);
|
|
1091
|
+
this.log("info", `notifyUser: fallback discovered session ${sessionKey} → calling sessions_send`);
|
|
1038
1092
|
try {
|
|
1039
1093
|
await (0, gateway_client_1.invokeGatewayTool)({
|
|
1040
1094
|
gateway: this.gatewayConfig,
|
|
@@ -1042,20 +1096,23 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
1042
1096
|
args: { sessionKey, message },
|
|
1043
1097
|
timeoutMs: 5_000,
|
|
1044
1098
|
});
|
|
1099
|
+
this.log("info", `notifyUser: ✓ fallback sessions_send to ${sessionKey} succeeded`);
|
|
1045
1100
|
this.addNotificationTarget(`web:${sessionKey}`, { type: "web", sessionKey });
|
|
1101
|
+
this.log("info", `notifyUser: registered ${sessionKey} as notification target for future use`);
|
|
1046
1102
|
}
|
|
1047
1103
|
catch (err) {
|
|
1048
|
-
this.log("
|
|
1104
|
+
this.log("error", `notifyUser: ✗ fallback sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1049
1105
|
}
|
|
1050
1106
|
}
|
|
1051
1107
|
else {
|
|
1052
|
-
this.log("warn", "notifyUser: no active session found, message lost");
|
|
1108
|
+
this.log("warn", "notifyUser: ✗ discoverActiveSession returned null — no active session found, message lost");
|
|
1053
1109
|
}
|
|
1054
1110
|
return;
|
|
1055
1111
|
}
|
|
1056
1112
|
const entries = [...this.notificationTargets.entries()];
|
|
1113
|
+
this.log("info", `notifyUser: sending to ${entries.length} registered target(s): [${entries.map(([k]) => k).join(", ")}]`);
|
|
1057
1114
|
const results = await Promise.allSettled(entries.map(async ([key, target]) => {
|
|
1058
|
-
this.log("info", `notifyUser:
|
|
1115
|
+
this.log("info", `notifyUser: → ${key} (type=${target.type})`);
|
|
1059
1116
|
try {
|
|
1060
1117
|
await (target.type === "channel"
|
|
1061
1118
|
? (0, gateway_client_1.invokeGatewayTool)({
|
|
@@ -1065,26 +1122,28 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
1065
1122
|
timeoutMs: 5_000,
|
|
1066
1123
|
})
|
|
1067
1124
|
: (0, gateway_client_1.invokeGatewayTool)({
|
|
1068
|
-
// sessions_send injects a message into the session so the AI
|
|
1069
|
-
// can relay it to the human (correct tool; was "chat.send" before)
|
|
1070
1125
|
gateway: this.gatewayConfig,
|
|
1071
1126
|
tool: "sessions_send",
|
|
1072
1127
|
args: { sessionKey: target.sessionKey, message },
|
|
1073
1128
|
timeoutMs: 5_000,
|
|
1074
1129
|
}));
|
|
1075
|
-
this.log("info", `notifyUser: ${key} (${target.type}) succeeded`);
|
|
1130
|
+
this.log("info", `notifyUser: ✓ ${key} (${target.type}) succeeded`);
|
|
1076
1131
|
}
|
|
1077
1132
|
catch (err) {
|
|
1078
|
-
this.log("
|
|
1133
|
+
this.log("error", `notifyUser: ✗ ${key} (${target.type}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1079
1134
|
throw err;
|
|
1080
1135
|
}
|
|
1081
1136
|
}));
|
|
1137
|
+
const okCount = results.filter((r) => r.status === "fulfilled").length;
|
|
1082
1138
|
const failCount = results.filter((r) => r.status === "rejected").length;
|
|
1083
|
-
if (failCount ===
|
|
1084
|
-
this.log("
|
|
1139
|
+
if (failCount === 0) {
|
|
1140
|
+
this.log("info", `notifyUser: ✓ all ${okCount} targets succeeded`);
|
|
1141
|
+
}
|
|
1142
|
+
else if (failCount === entries.length) {
|
|
1143
|
+
this.log("error", `notifyUser: ✗ ALL ${failCount} targets failed`);
|
|
1085
1144
|
}
|
|
1086
|
-
else
|
|
1087
|
-
this.log("warn", `notifyUser: ${failCount}
|
|
1145
|
+
else {
|
|
1146
|
+
this.log("warn", `notifyUser: ${okCount} ok, ${failCount} FAILED out of ${entries.length} targets`);
|
|
1088
1147
|
}
|
|
1089
1148
|
}
|
|
1090
1149
|
log(level, message) {
|
package/package.json
CHANGED
|
@@ -1,175 +1,182 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 会议协商调度 — 全自动多人协商
|
|
2
2
|
|
|
3
|
-
以下规则指导 AI 如何利用 MultiClaws
|
|
3
|
+
以下规则指导 AI 如何利用 MultiClaws 委派和 Google Calendar 实现多人自动协商会议。
|
|
4
|
+
核心原则:**如果所有参与者的 OpenClaw 都有日历工具,则全程无需人类干预。**
|
|
4
5
|
|
|
5
6
|
---
|
|
6
7
|
|
|
7
|
-
##
|
|
8
|
+
## 角色判断
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
- 约会议 / 安排会议 / 组织会议
|
|
11
|
-
- 查询团队成员空闲时间
|
|
12
|
-
- 找一个大家都有空的时间
|
|
10
|
+
你在会议协商中扮演两种角色之一:
|
|
13
11
|
|
|
14
|
-
|
|
12
|
+
| 触发条件 | 角色 |
|
|
13
|
+
|----------|------|
|
|
14
|
+
| 用户主动说「开会」「约会议」「安排会议」「schedule a meeting」等 | **组织者** |
|
|
15
|
+
| 收到委派任务,任务文本包含 `[会议协商-查询]` | **参与者 — 查询模式** |
|
|
16
|
+
| 收到委派任务,任务文本包含 `[会议协商-确认]` | **参与者 — 确认模式** |
|
|
15
17
|
|
|
16
18
|
---
|
|
17
19
|
|
|
18
|
-
##
|
|
20
|
+
## 组织者工作流
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
当用户发起会议请求时,按以下步骤**自动执行**,不要在每一步停下来询问用户。
|
|
23
|
+
|
|
24
|
+
### Step 1:解析需求
|
|
21
25
|
|
|
22
26
|
从用户请求中提取:
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
27
|
+
- **参会人**:指定的成员名单。「所有人」「整个团队」= 全部成员
|
|
28
|
+
- **时间范围**:用户期望的日期和时间段(如「明天下午」「周五 2-4 点」)
|
|
29
|
+
- **会议主题**:会议目的(可选,默认「团队会议」)
|
|
26
30
|
- **会议时长**:默认 1 小时,用户可指定
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
### 步骤 2:查询本地日历
|
|
32
|
+
**仅当用户未指定时间范围时,才询问用户。** 其他信息缺失可以使用默认值。
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
### Step 2:查询自己的日历
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
检测你是否有 Google Calendar 相关工具可用:
|
|
37
|
+
- **有日历工具** → 直接查询用户在指定时间范围内的事件,计算空闲时段。不要问用户是否要查,直接查
|
|
38
|
+
- **无日历工具** → 跳过此步,假设用户在指定时间范围内全部可用
|
|
37
39
|
|
|
38
|
-
###
|
|
40
|
+
### Step 3:获取团队成员
|
|
39
41
|
|
|
40
42
|
```
|
|
41
43
|
multiclaws_agents()
|
|
42
44
|
```
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
- 如果用户说「所有人」,取团队全部成员(排除自己)
|
|
49
|
-
- 找不到的成员告知用户
|
|
46
|
+
从返回的智能体列表中匹配参会人。匹配规则:
|
|
47
|
+
- 按 `ownerName` 或 `name` 模糊匹配
|
|
48
|
+
- 「所有人」= 全部成员(排除自己)
|
|
49
|
+
- 找不到的成员跳过并在最终汇报中说明
|
|
50
50
|
|
|
51
|
-
###
|
|
51
|
+
### Step 4:逐个委派查询可用时间
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
对每个参会成员调用 `multiclaws_delegate`,发送结构化查询请求:
|
|
54
54
|
|
|
55
55
|
```
|
|
56
56
|
multiclaws_delegate(
|
|
57
57
|
agentUrl="成员的agentUrl",
|
|
58
|
-
task="
|
|
59
|
-
请以如下格式回复:
|
|
60
|
-
- 空闲时段1:HH:MM - HH:MM
|
|
61
|
-
- 空闲时段2:HH:MM - HH:MM
|
|
62
|
-
如果完全没空,请说明。"
|
|
58
|
+
task="[会议协商-查询]\n会议主题:{主题}\n查询时间范围:{YYYY-MM-DD HH:MM} - {HH:MM} ({时区})\n会议时长:{N}小时\n发起人:{你的主人名字}\n\n请查看你主人的日历,回复 JSON 格式:\n{\"available\": true, \"freeSlots\": [\"HH:MM-HH:MM\", ...], \"timezone\": \"GMT+8\"}\n如果没有日历工具,请直接询问你的主人在该时间段是否有空。"
|
|
63
59
|
)
|
|
64
60
|
```
|
|
65
61
|
|
|
66
|
-
|
|
62
|
+
`multiclaws_delegate` 是同步阻塞的,会等待对方回复后才返回。逐个调用即可,无需轮询。
|
|
63
|
+
|
|
64
|
+
### Step 5:汇总与决策
|
|
65
|
+
|
|
66
|
+
收集所有成员的回复后,找出共同空闲时段:
|
|
67
|
+
|
|
68
|
+
1. 解析每个成员返回的 JSON(或自然语言描述的空闲时段)
|
|
69
|
+
2. 计算所有人(包括自己)的交集时段
|
|
70
|
+
3. **自动决策**,不要询问用户选哪个:
|
|
71
|
+
- 有全员可行的时段 → 选**最早的**一个,自动确定
|
|
72
|
+
- 多个等价时段 → 选**最早的**
|
|
73
|
+
- 无全员可行时段但部分可行 → 选覆盖人数最多的最早时段,自动确定
|
|
74
|
+
- 完全无可行时段 → **自动扩大时间范围 ±2 小时**,回到 Step 4 重新查询(仅重试一次)
|
|
75
|
+
- 扩大后仍无 → **这时才询问用户**,建议换日期或缩减参会人
|
|
67
76
|
|
|
68
|
-
###
|
|
77
|
+
### Step 6:创建日历事件
|
|
69
78
|
|
|
70
|
-
|
|
79
|
+
时间确定后:
|
|
80
|
+
- **有日历工具** → 直接创建事件(标题、时间、描述),不要询问用户是否创建
|
|
81
|
+
- **无日历工具** → 跳过,告知用户需要手动添加
|
|
71
82
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
4. 超时后检查各任务状态:
|
|
83
|
+
### Step 7:通知所有参会者
|
|
84
|
+
|
|
85
|
+
对每个参会成员调用 `multiclaws_delegate`,发送确认通知:
|
|
76
86
|
|
|
77
87
|
```
|
|
78
|
-
|
|
88
|
+
multiclaws_delegate(
|
|
89
|
+
agentUrl="成员的agentUrl",
|
|
90
|
+
task="[会议协商-确认]\n会议已确定:\n- 主题:{主题}\n- 时间:{YYYY-MM-DD HH:MM} - {HH:MM} ({时区})\n- 发起人:{你的主人名字}\n\n请在你主人的日历上创建此事件。如果没有日历工具,请告知你的主人这个会议时间。"
|
|
91
|
+
)
|
|
79
92
|
```
|
|
80
93
|
|
|
81
|
-
|
|
82
|
-
- ✅ **已回复**:提取空闲时段信息
|
|
83
|
-
- ⏳ **未回复(超时)**:标记该成员为「未响应」
|
|
84
|
-
- ❌ **失败**:标记该成员为「查询失败」
|
|
94
|
+
### Step 8:汇报
|
|
85
95
|
|
|
86
|
-
|
|
87
|
-
- 稍后重试
|
|
88
|
-
- 直接问相关人员的空闲时间
|
|
89
|
-
- 跳过未响应成员继续安排
|
|
96
|
+
向用户简洁汇报结果:
|
|
90
97
|
|
|
91
|
-
|
|
98
|
+
```
|
|
99
|
+
✅ 会议已安排
|
|
100
|
+
- 主题:{主题}
|
|
101
|
+
- 时间:{日期} {时段}
|
|
102
|
+
- 参会人:{名单}
|
|
103
|
+
- 日历事件:已创建 / 请手动添加
|
|
104
|
+
- 通知状态:全部已通知 / {某某}通知失败
|
|
105
|
+
```
|
|
92
106
|
|
|
93
|
-
|
|
107
|
+
---
|
|
94
108
|
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
## 参与者工作流 — 查询模式
|
|
110
|
+
|
|
111
|
+
当你收到的委派任务包含 `[会议协商-查询]` 时,执行以下流程:
|
|
97
112
|
|
|
98
|
-
|
|
99
|
-
|----------------|------|------|------|--------|
|
|
100
|
-
| 14:00 - 15:00 | ✅ | ✅ | ✅ | ✅ 全员可行 |
|
|
101
|
-
| 15:00 - 16:00 | ✅ | ❌ | ✅ | ⚠️ 小明不行 |
|
|
102
|
-
| 16:00 - 17:00 | ✅ | ✅ | ⏳ | ⚠️ 小红未响应 |
|
|
113
|
+
### 自动处理(有日历工具时)
|
|
103
114
|
|
|
104
|
-
|
|
115
|
+
1. 从任务文本中提取:时间范围、时区、会议时长
|
|
116
|
+
2. 使用 Google Calendar 工具查询该时间范围内的已有事件
|
|
117
|
+
3. 计算空闲时段(排除已有事件的时段,每个空闲段 ≥ 会议时长)
|
|
118
|
+
4. **直接返回结果**(通过 `multiclaws_a2a_callback`),不要询问用户确认:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{"available": true, "freeSlots": ["14:00-15:00", "15:30-16:00"], "timezone": "GMT+8"}
|
|
105
122
|
```
|
|
106
123
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
124
|
+
如果完全没有空闲:
|
|
125
|
+
```json
|
|
126
|
+
{"available": false, "freeSlots": [], "reason": "该时段已有会议", "timezone": "GMT+8"}
|
|
127
|
+
```
|
|
111
128
|
|
|
112
|
-
###
|
|
129
|
+
### 需要人工介入(无日历工具时)
|
|
113
130
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
- 会议标题需要调整吗?
|
|
118
|
-
- 是否需要添加会议描述或议程?
|
|
131
|
+
1. 告知用户:「收到来自 {发起人} 的会议邀请,主题:{主题},时间范围:{时间}。你在这个时间段有空吗?」
|
|
132
|
+
2. 等待用户回复
|
|
133
|
+
3. 将用户回答转换为结构化格式返回
|
|
119
134
|
|
|
120
|
-
|
|
135
|
+
---
|
|
121
136
|
|
|
122
|
-
|
|
137
|
+
## 参与者工作流 — 确认模式
|
|
123
138
|
|
|
124
|
-
|
|
125
|
-
1. 创建日历事件(标题、时间、描述)
|
|
126
|
-
2. 添加所有确认参会的成员为与会者(如果有邮箱信息)
|
|
127
|
-
3. 发送日历邀请
|
|
139
|
+
当你收到的委派任务包含 `[会议协商-确认]` 时:
|
|
128
140
|
|
|
129
|
-
###
|
|
141
|
+
### 自动处理(有日历工具时)
|
|
130
142
|
|
|
131
|
-
|
|
143
|
+
1. 从任务文本中提取:会议主题、确切时间、发起人
|
|
144
|
+
2. 使用 Google Calendar 工具**直接创建事件**,不要询问用户
|
|
145
|
+
3. 返回确认:`已将会议添加到日历`
|
|
132
146
|
|
|
133
|
-
|
|
134
|
-
multiclaws_delegate(
|
|
135
|
-
agentUrl="成员的agentUrl",
|
|
136
|
-
task="会议已确定:[主题],时间:[日期] [时段]。
|
|
137
|
-
请在你主人的日历上创建这个事件。"
|
|
138
|
-
)
|
|
139
|
-
```
|
|
147
|
+
### 需要人工介入(无日历工具时)
|
|
140
148
|
|
|
141
|
-
|
|
149
|
+
1. 告知用户:「{发起人} 安排了会议:{主题},时间:{时间}。请记得参加。」
|
|
150
|
+
2. 返回:`已通知用户`
|
|
142
151
|
|
|
143
152
|
---
|
|
144
153
|
|
|
145
|
-
##
|
|
154
|
+
## 边界情况
|
|
146
155
|
|
|
147
156
|
### 跨时区
|
|
148
|
-
-
|
|
149
|
-
-
|
|
157
|
+
- 委派查询时**必须附带时区**(如 GMT+8)
|
|
158
|
+
- 参与者回复时也附带自己的时区
|
|
159
|
+
- 组织者汇总时统一转换为自己的时区
|
|
150
160
|
|
|
151
|
-
###
|
|
152
|
-
-
|
|
153
|
-
- 简化确认流程
|
|
161
|
+
### 单人会议
|
|
162
|
+
- 只有一个参会者时,直接查询+确定,不需要表格汇总
|
|
154
163
|
|
|
155
164
|
### 用户取消
|
|
156
|
-
-
|
|
157
|
-
- 取消时不创建任何事件
|
|
165
|
+
- 在任何步骤用户说「算了」「取消」→ 立即停止,不创建事件,不发通知
|
|
158
166
|
|
|
159
|
-
###
|
|
160
|
-
-
|
|
161
|
-
|
|
167
|
+
### 委派超时或失败
|
|
168
|
+
- 某成员委派失败 → 跳过该成员,继续处理其他人
|
|
169
|
+
- 在最终汇报中注明哪些成员未能联系上
|
|
162
170
|
|
|
163
171
|
### 紧急会议
|
|
164
|
-
-
|
|
165
|
-
- 缩短超时为 1 分钟
|
|
172
|
+
- 用户说「现在」「马上」「尽快」→ 时间范围设为今天剩余时间,会议时长默认 30 分钟
|
|
166
173
|
|
|
167
174
|
---
|
|
168
175
|
|
|
169
|
-
##
|
|
176
|
+
## 关键规则
|
|
170
177
|
|
|
171
|
-
-
|
|
172
|
-
-
|
|
173
|
-
-
|
|
174
|
-
-
|
|
175
|
-
-
|
|
178
|
+
- **最小化人工干预**:有日历工具时,全程自动,不要停下来问用户"是否确认"
|
|
179
|
+
- **只在必要时才问**:仅当缺少关键信息(时间范围)或协商彻底失败时才询问用户
|
|
180
|
+
- **尊重隐私**:不在委派消息中透露其他成员的日程细节
|
|
181
|
+
- **使用标记**:委派任务必须包含 `[会议协商-查询]` 或 `[会议协商-确认]` 标记,让接收方 AI 识别角色
|
|
182
|
+
- **结构化通信**:查询回复使用 JSON 格式,便于组织者解析
|