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 sessions_spawn gateway tool.
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. Notifies the local human owner
25
- * 3. For risky tasks: waits for explicit human approval
26
- * For safe tasks: executes immediately
27
- * 4. Calls OpenClaw's `sessions_spawn` (run mode) to start execution
28
- * 5. Waits for the sub-agent to call back via `multiclaws_a2a_callback`
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 sessions_spawn gateway tool.
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. 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`
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
- // Classify risk and gate accordingly
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
- // Notify with approval request and wait
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} requesting human approval (timeout=${approvalTimeoutMs / 1000}s)`);
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
- // Task was explicitly canceled use the canonical "canceled" message
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 timed out auto-rejected`);
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} approved by user`);
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
- // 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)}`);
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
- // Create a promise that resolves when sub-agent calls multiclaws_a2a_callback
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
- // Spawn the subagent with instructions to call back when done
166
- const prompt = buildA2ASubagentPrompt(taskId, taskText);
167
- this.logger.info(`[a2a-adapter] task ${taskId} spawning sub-agent via sessions_spawn (cwd=${this.cwd}, sessionKey=a2a-${taskId})`);
168
- const spawnResult = await (0, gateway_client_1.invokeGatewayTool)({
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: "sessions_spawn",
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} sub-agent spawned result=${JSON.stringify(spawnResult).slice(0, 200)}`);
180
- this.logger.info(`[a2a-adapter] task ${taskId} waiting for callback from sub-agent...`);
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 and notify user
187
+ // ── Step 7: Return result ──
184
188
  this.taskTracker.update(taskId, { status: "completed", result: output });
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)}`);
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
- this.logger.error(`[a2a-adapter] task ${taskId} failed: ${errorMsg}`);
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: ${(s.key ?? s.sessionKey) ?? "(no key)"}`));
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.warn(`[a2a-adapter] discoverActiveSession failed: ${err instanceof Error ? err.message : String(err)}`);
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.info(`[a2a-adapter] notifyUser: skipped (no gateway config)`);
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 attempting session discovery`);
342
- const sessionKey = await this.discoverActiveSession();
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}, sending via sessions_send`);
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.warn(`[a2a-adapter] notifyUser: sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
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: sending to ${key} (type=${target.type})`);
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: sent to ${key} ✓`);
461
+ this.logger.info(`[a2a-adapter] notifyUser: ${key} (${target.type}) succeeded`);
385
462
  }
386
463
  catch (err) {
387
- this.logger.warn(`[a2a-adapter] notifyUser: failed to send to ${key}: ${err instanceof Error ? err.message : String(err)}`);
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
- this.logger.info(`[a2a-adapter] notifyUser: done (${ok} ok, ${fail} failed)`);
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 for the sub-agent that handles an incoming A2A task.
430
- * The sub-agent must call `multiclaws_a2a_callback` to report its result.
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 buildA2ASubagentPrompt(taskId, taskText) {
433
- return `你收到了一个来自远端智能体的委派任务。请完成任务并汇报结果。
434
-
435
- ## 任务内容
514
+ function buildA2AMainSessionPrompt(taskId, fromAgentName, taskText) {
515
+ return `[MultiClaws 委派任务] 来自 **${fromAgentName}**:
436
516
 
437
517
  ${taskText}
438
518
 
439
- ## 完成后必做
440
-
441
- 完成任务后,你**必须**调用 \`multiclaws_a2a_callback\` 工具汇报结果:
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
- await this.requireCompleteProfile();
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] task tracked: ${track.taskId}, status=running`);
284
+ this.log("info", `[delegate] [step:track] taskId=${track.taskId}, status=running`);
274
285
  try {
275
- this.log("info", `[delegate] ${track.taskId} creating A2A client for ${agentRecord.url}`);
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} A2A client created, starting fire-and-forget send`);
278
- // Fire-and-forget execution: keep running in the background so that
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 (background)...`);
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 (background)`);
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] ${track.taskId} background send failed: ${errorMsg}`);
308
+ this.log("error", `[delegate] ${track.taskId} [step:background-send] ✗ caught error: ${errorMsg} → task marked failed`);
299
309
  }
300
310
  })();
301
- // Return immediately so that gateway tool invocations are fast and
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] ${track.taskId} failed: ${errorMsg}`);
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
- await this.requireCompleteProfile();
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] task tracked: ${track.taskId}, status=running`);
352
+ this.log("info", `[delegate-sync] [step:track] taskId=${track.taskId}, status=running`);
334
353
  try {
335
- this.log("info", `[delegate-sync] ${track.taskId} creating A2A client for ${agentRecord.url}`);
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
- this.log("info", `[delegate-sync] ${track.taskId} sending A2A message (sync, with metadata: selfUrl=${this.selfUrl}, selfName=${this.agentCard?.name ?? "unknown"})...`);
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] ${track.taskId} completed status=${taskResult.status}, outputLen=${taskResult.output?.length ?? 0}`);
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] ${track.taskId} failed: ${errorMsg}`);
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
- await this.requireCompleteProfile();
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] spawning sub-agent via sessions_spawn (cwd=${this.resolvedCwd}, sessionKey=${sessionKey}, promptLen=${prompt.length})`);
384
- const spawnResult = await (0, gateway_client_1.invokeGatewayTool)({
385
- gateway: this.gatewayConfig,
386
- tool: "sessions_spawn",
387
- args: { task: prompt, mode: "run", cwd: this.resolvedCwd },
388
- sessionKey,
389
- timeoutMs: 15_000,
390
- });
391
- this.log("info", `[spawn-delegate] ✓ sub-agent spawned for ${agent.name} — result=${JSON.stringify(spawnResult).slice(0, 200)}`);
392
- return { message: `已启动子 agent ${agent.name} 委派任务` };
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
- return await this.clientFactory.createFromUrl(agent.url);
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", ` session: ${(s.key ?? s.sessionKey) ?? "(no key)"}`));
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("warn", `discoverActiveSession failed: ${err instanceof Error ? err.message : String(err)}`);
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}, msg=${message.slice(0, 80)}`);
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("warn", "notifyUser: no registered targets attempting session discovery");
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("warn", `notifyUser: sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
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: sending to ${key} (type=${target.type})`);
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("warn", `notifyUser: ${key} (${target.type}) failed: ${err instanceof Error ? err.message : String(err)}`);
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 === entries.length) {
1084
- this.log("error", `notifyUser: ALL ${failCount} targets failed`);
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 if (failCount > 0) {
1087
- this.log("warn", `notifyUser: ${failCount}/${entries.length} targets failed`);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "multiclaws",
3
- "version": "0.4.40",
3
+ "version": "0.4.42",
4
4
  "description": "MultiClaws plugin for OpenClaw collaboration via A2A protocol",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,175 +1,182 @@
1
- # 会议调度技能行为
1
+ # 会议协商调度全自动多人协商
2
2
 
3
- 以下规则指导 AI 如何利用 MultiClaws 团队协作和 Google Calendar 完成会议调度。
3
+ 以下规则指导 AI 如何利用 MultiClaws 委派和 Google Calendar 实现多人自动协商会议。
4
+ 核心原则:**如果所有参与者的 OpenClaw 都有日历工具,则全程无需人类干预。**
4
5
 
5
6
  ---
6
7
 
7
- ## 触发条件
8
+ ## 角色判断
8
9
 
9
- 当用户的请求包含以下意图时激活此 Skill:
10
- - 约会议 / 安排会议 / 组织会议
11
- - 查询团队成员空闲时间
12
- - 找一个大家都有空的时间
10
+ 你在会议协商中扮演两种角色之一:
13
11
 
14
- **关键词示例:** 「约会议」「开会」「安排一个会」「找个时间碰一下」「schedule a meeting」
12
+ | 触发条件 | 角色 |
13
+ |----------|------|
14
+ | 用户主动说「开会」「约会议」「安排会议」「schedule a meeting」等 | **组织者** |
15
+ | 收到委派任务,任务文本包含 `[会议协商-查询]` | **参与者 — 查询模式** |
16
+ | 收到委派任务,任务文本包含 `[会议协商-确认]` | **参与者 — 确认模式** |
15
17
 
16
18
  ---
17
19
 
18
- ## 工作流
20
+ ## 组织者工作流
19
21
 
20
- ### 步骤 1:解析会议需求
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
- 使用 Google Calendar 工具读取**用户自己**在指定时间范围内的日程:
34
+ ### Step 2:查询自己的日历
33
35
 
34
- 1. 获取指定时间范围内的所有事件
35
- 2. 计算用户的空闲时段
36
- 3. 如果用户在该时间范围内完全没空,提前告知用户并建议换个时间段
36
+ 检测你是否有 Google Calendar 相关工具可用:
37
+ - **有日历工具** → 直接查询用户在指定时间范围内的事件,计算空闲时段。不要问用户是否要查,直接查
38
+ - **无日历工具** → 跳过此步,假设用户在指定时间范围内全部可用
37
39
 
38
- ### 步骤 3:获取团队成员信息
40
+ ### Step 3:获取团队成员
39
41
 
40
42
  ```
41
43
  multiclaws_agents()
42
44
  ```
43
45
 
44
- 从返回的智能体列表中,根据用户指定的参会人名单匹配对应的智能体。
45
-
46
- **匹配规则:**
47
- - 优先按 `ownerName` 匹配
48
- - 如果用户说「所有人」,取团队全部成员(排除自己)
49
- - 找不到的成员告知用户
46
+ 从返回的智能体列表中匹配参会人。匹配规则:
47
+ - 按 `ownerName` 或 `name` 模糊匹配
48
+ - 「所有人」= 全部成员(排除自己)
49
+ - 找不到的成员跳过并在最终汇报中说明
50
50
 
51
- ### 步骤 4:并行委派查询空闲时间
51
+ ### Step 4:逐个委派查询可用时间
52
52
 
53
- 对每个参会成员**分别**调用 `multiclaws_delegate`:
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
- 记录每个委派返回的 `taskId`。
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
- ### 步骤 5:收集回复(超时机制)
77
+ ### Step 6:创建日历事件
69
78
 
70
- **等待策略:手动检查 + 自动超时**
79
+ 时间确定后:
80
+ - **有日历工具** → 直接创建事件(标题、时间、描述),不要询问用户是否创建
81
+ - **无日历工具** → 跳过,告知用户需要手动添加
71
82
 
72
- 1. 委派发出后告知用户:「已向 N 位成员查询空闲时间,等待回复中…」
73
- 2. 设定超时:**3 分钟**
74
- 3. 在超时前,如有结果通过子 agent 消息返回则自动收集
75
- 4. 超时后检查各任务状态:
83
+ ### Step 7:通知所有参会者
84
+
85
+ 对每个参会成员调用 `multiclaws_delegate`,发送确认通知:
76
86
 
77
87
  ```
78
- multiclaws_task_status(taskId="xxx")
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
- ### 步骤 6:汇总与展示
98
+ ```
99
+ ✅ 会议已安排
100
+ - 主题:{主题}
101
+ - 时间:{日期} {时段}
102
+ - 参会人:{名单}
103
+ - 日历事件:已创建 / 请手动添加
104
+ - 通知状态:全部已通知 / {某某}通知失败
105
+ ```
92
106
 
93
- 将所有结果汇总为易读的表格,展示给用户:
107
+ ---
94
108
 
95
- ```
96
- 📅 会议空闲时段汇总(主题:xxx)
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
- 推荐时段:14:00 - 15:00(全员可行)
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
- 1. 优先推荐全员都空闲的时段
109
- 2. 若无全员空闲时段,推荐覆盖人数最多的时段
110
- 3. 若有多个等价时段,推荐最早的
124
+ 如果完全没有空闲:
125
+ ```json
126
+ {"available": false, "freeSlots": [], "reason": "该时段已有会议", "timezone": "GMT+8"}
127
+ ```
111
128
 
112
- ### 步骤 7:用户确认
129
+ ### 需要人工介入(无日历工具时)
113
130
 
114
- 询问用户:
115
- - 选择哪个时段?
116
- - 是否接受部分成员缺席?
117
- - 会议标题需要调整吗?
118
- - 是否需要添加会议描述或议程?
131
+ 1. 告知用户:「收到来自 {发起人} 的会议邀请,主题:{主题},时间范围:{时间}。你在这个时间段有空吗?」
132
+ 2. 等待用户回复
133
+ 3. 将用户回答转换为结构化格式返回
119
134
 
120
- 等待用户明确确认后才继续。
135
+ ---
121
136
 
122
- ### 步骤 8:创建日历事件
137
+ ## 参与者工作流 — 确认模式
123
138
 
124
- 用户确认后,使用 Google Calendar 工具:
125
- 1. 创建日历事件(标题、时间、描述)
126
- 2. 添加所有确认参会的成员为与会者(如果有邮箱信息)
127
- 3. 发送日历邀请
139
+ 当你收到的委派任务包含 `[会议协商-确认]` 时:
128
140
 
129
- ### 步骤 9:通知参会者
141
+ ### 自动处理(有日历工具时)
130
142
 
131
- 对每个确认参会的远端成员调用 `multiclaws_delegate`,通知会议已确定:
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
- - 委派查询时明确指定时区(如 GMT+8)
149
- - 汇总时统一转换为用户所在时区展示
157
+ - 委派查询时**必须附带时区**(如 GMT+8)
158
+ - 参与者回复时也附带自己的时区
159
+ - 组织者汇总时统一转换为自己的时区
150
160
 
151
- ### 只有一个参会者
152
- - 跳过汇总表格,直接展示双方都空闲的时段
153
- - 简化确认流程
161
+ ### 单人会议
162
+ - 只有一个参会者时,直接查询+确定,不需要表格汇总
154
163
 
155
164
  ### 用户取消
156
- - 在任何步骤用户都可以说「算了」「取消」
157
- - 取消时不创建任何事件
165
+ - 在任何步骤用户说「算了」「取消」→ 立即停止,不创建事件,不发通知
158
166
 
159
- ### 成员无日历能力
160
- - 如果某成员的 bio 中没有提到日历相关能力,在委派时明确说明:
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
- - **汇总要直观**,用表格和 emoji 使结果一目了然
174
- - **尊重隐私**,不在委派消息中透露其他成员的具体日程细节
175
- - **保持礼貌**,委派消息用协商语气而非命令语气
178
+ - **最小化人工干预**:有日历工具时,全程自动,不要停下来问用户"是否确认"
179
+ - **只在必要时才问**:仅当缺少关键信息(时间范围)或协商彻底失败时才询问用户
180
+ - **尊重隐私**:不在委派消息中透露其他成员的日程细节
181
+ - **使用标记**:委派任务必须包含 `[会议协商-查询]` 或 `[会议协商-确认]` 标记,让接收方 AI 识别角色
182
+ - **结构化通信**:查询回复使用 JSON 格式,便于组织者解析