multiclaws 0.4.30 → 0.4.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -200,6 +200,40 @@ function createTools(getService, logger) {
200
200
  }
201
201
  },
202
202
  };
203
+ const multiclawsA2ACallback = {
204
+ name: "multiclaws_a2a_callback",
205
+ description: "Report the result of an incoming A2A delegated task. " +
206
+ "Called by sub-agents spawned to handle remote tasks. " +
207
+ "Do NOT call this directly.",
208
+ parameters: {
209
+ type: "object",
210
+ additionalProperties: false,
211
+ properties: {
212
+ taskId: { type: "string" },
213
+ result: { type: "string" },
214
+ },
215
+ required: ["taskId", "result"],
216
+ },
217
+ execute: async (_toolCallId, args) => {
218
+ const taskId = typeof args.taskId === "string" ? args.taskId.trim() : "";
219
+ const result = typeof args.result === "string" ? args.result : "";
220
+ log("info", `tool:multiclaws_a2a_callback(taskId=${taskId})`);
221
+ try {
222
+ const service = requireService(getService());
223
+ if (!taskId || !result)
224
+ throw new Error("taskId and result are required");
225
+ const resolved = service.resolveA2ACallback(taskId, result);
226
+ if (!resolved) {
227
+ return textResult(`No pending callback for task ${taskId}. It may have timed out.`);
228
+ }
229
+ return textResult(`Task ${taskId} result reported successfully.`);
230
+ }
231
+ catch (err) {
232
+ log("error", `tool:multiclaws_a2a_callback failed: ${err instanceof Error ? err.message : String(err)}`);
233
+ throw err;
234
+ }
235
+ },
236
+ };
203
237
  const multiclawsTaskStatus = {
204
238
  name: "multiclaws_task_status",
205
239
  description: "Check the status of a delegated task.",
@@ -436,6 +470,7 @@ function createTools(getService, logger) {
436
470
  multiclawsRemoveAgent,
437
471
  multiclawsDelegate,
438
472
  multiclawsDelegateSend,
473
+ multiclawsA2ACallback,
439
474
  multiclawsTaskStatus,
440
475
  multiclawsTeamCreate,
441
476
  multiclawsTeamJoin,
@@ -458,16 +493,34 @@ const plugin = {
458
493
  let service = null;
459
494
  // Ensure required tools are in gateway.tools.allow at registration time
460
495
  // so the gateway starts with them already present (no restart needed).
496
+ //
497
+ // Two categories:
498
+ // 1. Adapter-internal tools: sessions_spawn, sessions_history, message
499
+ // — needed by a2a-adapter itself to spawn/poll/notify.
500
+ // 2. A2A execution tools: exec, read, write, glob, grep
501
+ // — needed by spawned sub-agents to actually perform delegated tasks.
502
+ // Without these the sub-agent session hits "permission denied" because
503
+ // gateway.tools.allow restricts which tools the session can invoke.
504
+ //
505
+ // Users can override the execution tool list via plugin config:
506
+ // plugins.multiclaws.a2aAllowedTools: ["exec", "read", "write", ...]
461
507
  if (api.config) {
462
508
  const gw = api.config.gateway;
463
509
  if (gw) {
464
510
  const tools = (gw.tools ?? {});
465
511
  const allow = Array.isArray(tools.allow) ? tools.allow : [];
466
- const required = ["sessions_spawn", "sessions_history", "message"];
512
+ const adapterRequired = ["sessions_spawn", "sessions_history", "message", "chat.send"];
513
+ const defaultA2AExecutionTools = ["exec", "read", "write", "edit", "process"];
514
+ const pluginConf = api.pluginConfig ?? {};
515
+ const a2aExecTools = Array.isArray(pluginConf.a2aAllowedTools)
516
+ ? pluginConf.a2aAllowedTools
517
+ : defaultA2AExecutionTools;
518
+ const required = [...new Set([...adapterRequired, ...a2aExecTools])];
467
519
  const missing = required.filter((t) => !allow.includes(t));
468
520
  if (missing.length > 0) {
469
521
  tools.allow = [...allow, ...missing];
470
522
  gw.tools = tools;
523
+ structured.logger.info(`auto-added gateway tools: ${missing.join(", ")}`);
471
524
  }
472
525
  }
473
526
  }
@@ -542,14 +595,18 @@ const plugin = {
542
595
  api.on("gateway_stop", () => {
543
596
  structured.logger.info("[multiclaws] gateway_stop observed");
544
597
  });
545
- // Collect all channel IDs for broadcasting notifications
598
+ // Collect notification targets from incoming messages (external channels)
546
599
  api.on("message_received", (_event, ctx) => {
547
- if (service && ctx.channelId) {
548
- service.addChannelId(ctx.channelId);
600
+ if (service && ctx.channelId && ctx.channelId !== "webchat" && ctx.conversationId) {
601
+ service.addNotificationTarget(`${ctx.channelId}:${ctx.conversationId}`, { type: "channel", conversationId: ctx.conversationId });
549
602
  }
550
603
  });
551
604
  // Inject onboarding prompt when profile is pending first-run setup
552
- api.on("before_prompt_build", async (_event, _ctx) => {
605
+ // Also capture web session targets for notifications
606
+ api.on("before_prompt_build", async (_event, ctx) => {
607
+ if (service && ctx.sessionKey) {
608
+ service.addNotificationTarget(`web:${ctx.sessionKey}`, { type: "web", sessionKey: ctx.sessionKey });
609
+ }
553
610
  if (!service)
554
611
  return;
555
612
  try {
@@ -1,11 +1,12 @@
1
1
  import type { AgentExecutor, ExecutionEventBus, RequestContext } from "@a2a-js/sdk/server";
2
2
  import { type GatewayConfig } from "../infra/gateway-client";
3
3
  import type { TaskTracker } from "../task/tracker";
4
+ import type { NotificationTarget } from "./multiclaws-service";
4
5
  export type A2AAdapterOptions = {
5
6
  gatewayConfig: GatewayConfig | null;
6
7
  taskTracker: TaskTracker;
7
8
  cwd?: string;
8
- getChannelIds?: () => ReadonlySet<string>;
9
+ getNotificationTargets?: () => ReadonlyMap<string, NotificationTarget>;
9
10
  logger: {
10
11
  info: (msg: string) => void;
11
12
  warn: (msg: string) => void;
@@ -19,35 +20,31 @@ export type A2AAdapterOptions = {
19
20
  * this executor:
20
21
  * 1. Records the task via TaskTracker
21
22
  * 2. Calls OpenClaw's `sessions_spawn` (run mode) to start execution
22
- * 3. Polls `sessions_history` until the subagent completes
23
+ * 3. Waits for the sub-agent to call back via `multiclaws_a2a_callback`
23
24
  * 4. Returns the final result as a Message
24
25
  */
25
26
  export declare class OpenClawAgentExecutor implements AgentExecutor {
26
27
  private gatewayConfig;
27
28
  private readonly taskTracker;
28
- private readonly getChannelIds;
29
+ private readonly getNotificationTargets;
29
30
  private readonly logger;
30
31
  private readonly cwd;
32
+ private readonly pendingCallbacks;
31
33
  constructor(options: A2AAdapterOptions);
32
34
  execute(context: RequestContext, eventBus: ExecutionEventBus): Promise<void>;
33
35
  /**
34
- * Poll sessions_history until the subagent session completes.
35
- * Collects ALL assistant text messages and returns them joined.
36
+ * Called by the `multiclaws_a2a_callback` tool when a sub-agent reports its result.
37
+ * Returns true if a pending callback was found and resolved.
36
38
  */
37
- private waitForCompletion;
38
- /**
39
- * Extract all assistant text from session history once the session is complete.
40
- * Returns null if the session is still running.
41
- * Returns all assistant text messages joined (not just the last one).
42
- *
43
- * Gateway /tools/invoke returns: { content: [...], details: { messages: [...], isComplete?: boolean } }
44
- */
45
- private extractCompletedResult;
46
- /** Extract text content from a single history message. */
47
- private extractTextFromHistoryMessage;
39
+ resolveCallback(taskId: string, result: string): boolean;
48
40
  cancelTask(taskId: string, eventBus: ExecutionEventBus): Promise<void>;
49
41
  updateGatewayConfig(config: GatewayConfig): void;
50
- /** Send a notification to all known channels. Individual failures are silently ignored. */
42
+ /**
43
+ * Create a pending callback that resolves when the sub-agent reports back,
44
+ * or rejects on timeout.
45
+ */
46
+ private createCallback;
47
+ /** Send a notification to all known targets. Individual failures are silently ignored. */
51
48
  private notifyUser;
52
49
  private publishMessage;
53
50
  }
@@ -10,24 +10,6 @@ function extractTextFromMessage(message) {
10
10
  .map((p) => p.text)
11
11
  .join("\n");
12
12
  }
13
- function sleep(ms) {
14
- return new Promise((resolve) => setTimeout(resolve, ms));
15
- }
16
- /**
17
- * Extract the details object from a gateway /tools/invoke result.
18
- * The result shape is: { content: [...], details: { ...actual data... } }
19
- */
20
- function extractDetails(result) {
21
- if (!result || typeof result !== "object")
22
- return null;
23
- const r = result;
24
- // Direct details from /tools/invoke
25
- if (r.details && typeof r.details === "object") {
26
- return r.details;
27
- }
28
- // Fallback: result itself might be the details
29
- return r;
30
- }
31
13
  /**
32
14
  * Bridges the A2A protocol to OpenClaw's sessions_spawn gateway tool.
33
15
  *
@@ -35,19 +17,20 @@ function extractDetails(result) {
35
17
  * this executor:
36
18
  * 1. Records the task via TaskTracker
37
19
  * 2. Calls OpenClaw's `sessions_spawn` (run mode) to start execution
38
- * 3. Polls `sessions_history` until the subagent completes
20
+ * 3. Waits for the sub-agent to call back via `multiclaws_a2a_callback`
39
21
  * 4. Returns the final result as a Message
40
22
  */
41
23
  class OpenClawAgentExecutor {
42
24
  gatewayConfig;
43
25
  taskTracker;
44
- getChannelIds;
26
+ getNotificationTargets;
45
27
  logger;
46
28
  cwd;
29
+ pendingCallbacks = new Map();
47
30
  constructor(options) {
48
31
  this.gatewayConfig = options.gatewayConfig;
49
32
  this.taskTracker = options.taskTracker;
50
- this.getChannelIds = options.getChannelIds ?? (() => new Set());
33
+ this.getNotificationTargets = options.getNotificationTargets ?? (() => new Map());
51
34
  this.logger = options.logger;
52
35
  this.cwd = options.cwd || process.cwd();
53
36
  }
@@ -76,34 +59,27 @@ class OpenClawAgentExecutor {
76
59
  void this.notifyUser(`📨 收到来自 **${fromAgent}** 的委派任务:${taskText.slice(0, 200)}`);
77
60
  try {
78
61
  this.logger.info(`[a2a-adapter] executing task ${taskId}: ${taskText.slice(0, 100)}`);
79
- // 1. Spawn the subagent
80
- // Use a dedicated session key to avoid inheriting the main session's
81
- // thinking blocks, which would cause "thinking blocks cannot be modified"
82
- // errors from the Claude API.
83
- const spawnResult = await (0, gateway_client_1.invokeGatewayTool)({
62
+ // Create a promise that resolves when sub-agent calls multiclaws_a2a_callback
63
+ const resultPromise = this.createCallback(taskId, 180_000);
64
+ // Spawn the subagent with instructions to call back when done
65
+ const prompt = buildA2ASubagentPrompt(taskId, taskText);
66
+ await (0, gateway_client_1.invokeGatewayTool)({
84
67
  gateway: this.gatewayConfig,
85
68
  tool: "sessions_spawn",
86
69
  args: {
87
- task: taskText,
70
+ task: prompt,
88
71
  mode: "run",
89
72
  cwd: this.cwd,
90
73
  },
91
74
  sessionKey: `a2a-${taskId}`,
92
75
  timeoutMs: 15_000,
93
76
  });
94
- // Extract details from gateway response: { content: [...], details: { childSessionKey, ... } }
95
- const details = extractDetails(spawnResult);
96
- const childSessionKey = details?.childSessionKey;
97
- if (!childSessionKey) {
98
- throw new Error("sessions_spawn did not return a childSessionKey");
99
- }
100
- // 2. Poll for completion
101
- const gatewaySessionKey = `a2a-${taskId}`;
102
- this.logger.info(`[a2a-adapter] task ${taskId} spawned as ${childSessionKey}, waiting for result...`);
103
- const output = await this.waitForCompletion(childSessionKey, 180_000, gatewaySessionKey);
104
- // 3. Return result
77
+ this.logger.info(`[a2a-adapter] task ${taskId} spawned, waiting for callback...`);
78
+ // Wait for the sub-agent to call back
79
+ const output = await resultPromise;
80
+ // Return result
105
81
  this.taskTracker.update(taskId, { status: "completed", result: output });
106
- this.logger.info(`[a2a-adapter] task ${taskId} completed`);
82
+ this.logger.info(`[a2a-adapter] task ${taskId} completed, resultLen=${output.length}`);
107
83
  this.publishMessage(eventBus, output || "Task completed with no output.");
108
84
  }
109
85
  catch (err) {
@@ -115,146 +91,27 @@ class OpenClawAgentExecutor {
115
91
  eventBus.finished();
116
92
  }
117
93
  /**
118
- * Poll sessions_history until the subagent session completes.
119
- * Collects ALL assistant text messages and returns them joined.
120
- */
121
- async waitForCompletion(sessionKey, timeoutMs, gatewaySessionKey) {
122
- this.logger.info(`[a2a-adapter] waitForCompletion(sessionKey=${sessionKey}, timeoutMs=${timeoutMs})`);
123
- const gateway = this.gatewayConfig;
124
- const startTime = Date.now();
125
- let attempt = 0;
126
- const pollDelays = [100, 200, 300, 500];
127
- while (Date.now() - startTime < timeoutMs) {
128
- const delay = pollDelays[Math.min(attempt, pollDelays.length - 1)];
129
- await sleep(delay);
130
- attempt++;
131
- try {
132
- const histResult = await (0, gateway_client_1.invokeGatewayTool)({
133
- gateway,
134
- tool: "sessions_history",
135
- args: {
136
- sessionKey,
137
- limit: 50,
138
- includeTools: false,
139
- },
140
- sessionKey: gatewaySessionKey,
141
- timeoutMs: 8_000,
142
- });
143
- const result = this.extractCompletedResult(histResult);
144
- if (result !== null) {
145
- this.logger.info(`[a2a-adapter] poll attempt ${attempt}: session ${sessionKey} completed, resultLen=${result.length}`);
146
- return result;
147
- }
148
- // Log details on first attempt, then every 10 attempts for diagnosis
149
- if (attempt === 1 || attempt % 10 === 0) {
150
- const details = extractDetails(histResult);
151
- const messages = (details?.messages ?? []);
152
- const lastMsg = messages[messages.length - 1];
153
- this.logger.info(`[a2a-adapter] poll attempt ${attempt}: session ${sessionKey} still running. ` +
154
- `isComplete=${details?.isComplete}, status=${details?.status}, ` +
155
- `msgCount=${messages.length}, lastRole=${lastMsg?.role}, ` +
156
- `lastContentTypes=${JSON.stringify(Array.isArray(lastMsg?.content)
157
- ? lastMsg.content.map((c) => c?.type)
158
- : typeof lastMsg?.content)}`);
159
- }
160
- else {
161
- this.logger.info(`[a2a-adapter] poll attempt ${attempt}: session ${sessionKey} still running...`);
162
- }
163
- }
164
- catch (err) {
165
- this.logger.warn(`[a2a-adapter] poll attempt ${attempt} error: ${err instanceof Error ? err.message : err}`);
166
- }
167
- }
168
- const elapsed = Math.round((Date.now() - startTime) / 1000);
169
- this.logger.error(`[a2a-adapter] waitForCompletion timed out: session=${sessionKey}, elapsed=${elapsed}s, attempts=${attempt}`);
170
- throw new Error(`task timed out after ${elapsed}s (${attempt} attempts) waiting for subagent`);
171
- }
172
- /**
173
- * Extract all assistant text from session history once the session is complete.
174
- * Returns null if the session is still running.
175
- * Returns all assistant text messages joined (not just the last one).
176
- *
177
- * Gateway /tools/invoke returns: { content: [...], details: { messages: [...], isComplete?: boolean } }
94
+ * Called by the `multiclaws_a2a_callback` tool when a sub-agent reports its result.
95
+ * Returns true if a pending callback was found and resolved.
178
96
  */
179
- extractCompletedResult(histResult) {
180
- const details = extractDetails(histResult);
181
- if (!details)
182
- return null;
183
- // Check for session-level error/status from gateway
184
- const sessionError = details.error;
185
- const sessionStatus = details.status;
186
- // Immediately fail on terminal session statuses — do NOT keep polling
187
- const terminalStatuses = ["forbidden", "failed", "error", "not_found", "unauthorized"];
188
- if (sessionStatus && terminalStatuses.includes(sessionStatus)) {
189
- this.logger.warn(`[a2a-adapter] extractCompletedResult: terminal status="${sessionStatus}", error="${sessionError ?? "none"}"`);
190
- return `Error: session status "${sessionStatus}"${sessionError ? `: ${sessionError}` : ""}`;
191
- }
192
- const messages = (details.messages ?? []);
193
- if (messages.length === 0 && !details.isComplete)
194
- return null;
195
- // If session is not explicitly complete, use heuristic: check if the session is still executing
196
- if (details.isComplete !== true) {
197
- if (messages.length === 0)
198
- return null;
199
- const lastMsg = messages[messages.length - 1];
200
- if (lastMsg && Array.isArray(lastMsg.content)) {
201
- const content = lastMsg.content;
202
- const hasToolCalls = content.some((c) => c?.type === "toolCall" || c?.type === "tool_use");
203
- // If the last message only has tool calls (no text), still running
204
- const hasText = content.some((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim());
205
- if (hasToolCalls && !hasText)
206
- return null;
207
- }
208
- // If the last message is a user message, the agent hasn't responded yet
209
- if (lastMsg?.role === "user")
210
- return null;
211
- }
212
- // Session is complete — collect ALL assistant text messages in order
213
- const allTexts = [];
214
- for (const msg of messages) {
215
- if (msg.role !== "assistant")
216
- continue;
217
- const text = this.extractTextFromHistoryMessage(msg);
218
- if (text)
219
- allTexts.push(text);
220
- }
221
- // If we have assistant text, return it (even if there's also an error)
222
- if (allTexts.length > 0) {
223
- // Append error info if present so the delegating agent sees both
224
- if (sessionError) {
225
- allTexts.push(`[session error: ${sessionError}]`);
226
- }
227
- return allTexts.join("\n\n");
228
- }
229
- // No assistant text — check if the session reported an error
230
- if (sessionError) {
231
- return `Error: ${sessionError}`;
232
- }
233
- if (sessionStatus === "failed" || sessionStatus === "error") {
234
- return `Error: session ended with status "${sessionStatus}"`;
235
- }
236
- // Session truly completed with no output at all
237
- return "(task completed with no text output)";
238
- }
239
- /** Extract text content from a single history message. */
240
- extractTextFromHistoryMessage(msg) {
241
- const content = msg.content;
242
- if (typeof content === "string" && content.trim()) {
243
- return content;
244
- }
245
- if (Array.isArray(content)) {
246
- const parts = content;
247
- const textParts = parts
248
- .filter((c) => c?.type === "text" && typeof c.text === "string" && c.text.trim())
249
- .map((c) => c.text);
250
- if (textParts.length > 0) {
251
- return textParts.join("\n");
252
- }
253
- }
254
- return null;
97
+ resolveCallback(taskId, result) {
98
+ const pending = this.pendingCallbacks.get(taskId);
99
+ if (!pending)
100
+ return false;
101
+ clearTimeout(pending.timer);
102
+ this.pendingCallbacks.delete(taskId);
103
+ pending.resolve(result);
104
+ return true;
255
105
  }
256
106
  async cancelTask(taskId, eventBus) {
257
107
  this.logger.info(`[a2a-adapter] cancelTask(taskId=${taskId})`);
108
+ // Reject pending callback if any
109
+ const pending = this.pendingCallbacks.get(taskId);
110
+ if (pending) {
111
+ clearTimeout(pending.timer);
112
+ this.pendingCallbacks.delete(taskId);
113
+ pending.reject(new Error("canceled"));
114
+ }
258
115
  this.taskTracker.update(taskId, { status: "failed", error: "canceled" });
259
116
  this.publishMessage(eventBus, "Task was canceled.");
260
117
  eventBus.finished();
@@ -262,17 +119,37 @@ class OpenClawAgentExecutor {
262
119
  updateGatewayConfig(config) {
263
120
  this.gatewayConfig = config;
264
121
  }
265
- /** Send a notification to all known channels. Individual failures are silently ignored. */
122
+ /**
123
+ * Create a pending callback that resolves when the sub-agent reports back,
124
+ * or rejects on timeout.
125
+ */
126
+ createCallback(taskId, timeoutMs) {
127
+ return new Promise((resolve, reject) => {
128
+ const timer = setTimeout(() => {
129
+ this.pendingCallbacks.delete(taskId);
130
+ reject(new Error(`task timed out after ${timeoutMs / 1000}s waiting for sub-agent callback`));
131
+ }, timeoutMs);
132
+ this.pendingCallbacks.set(taskId, { resolve, reject, timer });
133
+ });
134
+ }
135
+ /** Send a notification to all known targets. Individual failures are silently ignored. */
266
136
  async notifyUser(message) {
267
- const channels = this.getChannelIds();
268
- if (!this.gatewayConfig || channels.size === 0)
137
+ const targets = this.getNotificationTargets();
138
+ if (!this.gatewayConfig || targets.size === 0)
269
139
  return;
270
- await Promise.allSettled([...channels].map((target) => (0, gateway_client_1.invokeGatewayTool)({
271
- gateway: this.gatewayConfig,
272
- tool: "message",
273
- args: { action: "send", target, message },
274
- timeoutMs: 5_000,
275
- })));
140
+ await Promise.allSettled([...targets.values()].map((target) => target.type === "channel"
141
+ ? (0, gateway_client_1.invokeGatewayTool)({
142
+ gateway: this.gatewayConfig,
143
+ tool: "message",
144
+ args: { action: "send", target: target.conversationId, message },
145
+ timeoutMs: 5_000,
146
+ })
147
+ : (0, gateway_client_1.invokeGatewayTool)({
148
+ gateway: this.gatewayConfig,
149
+ tool: "chat.send",
150
+ args: { sessionKey: target.sessionKey, message },
151
+ timeoutMs: 5_000,
152
+ })));
276
153
  }
277
154
  publishMessage(eventBus, text) {
278
155
  const message = {
@@ -285,3 +162,28 @@ class OpenClawAgentExecutor {
285
162
  }
286
163
  }
287
164
  exports.OpenClawAgentExecutor = OpenClawAgentExecutor;
165
+ /**
166
+ * Build the prompt for the sub-agent that handles an incoming A2A task.
167
+ * The sub-agent must call `multiclaws_a2a_callback` to report its result.
168
+ */
169
+ function buildA2ASubagentPrompt(taskId, taskText) {
170
+ return `你收到了一个来自远端智能体的委派任务。请完成任务并汇报结果。
171
+
172
+ ## 任务内容
173
+
174
+ ${taskText}
175
+
176
+ ## 完成后必做
177
+
178
+ 完成任务后,你**必须**调用 \`multiclaws_a2a_callback\` 工具汇报结果:
179
+
180
+ \`\`\`
181
+ multiclaws_a2a_callback(taskId="${taskId}", result="你的完整回复内容")
182
+ \`\`\`
183
+
184
+ **重要**:
185
+ - 无论任务成功还是失败,都必须调用 \`multiclaws_a2a_callback\`
186
+ - result 参数填写你的完整回复文本
187
+ - 如果任务失败,在 result 中说明失败原因
188
+ - 这是唯一的结果回传方式,不调用则结果会丢失`;
189
+ }
@@ -27,6 +27,13 @@ export type DelegateTaskResult = {
27
27
  status: string;
28
28
  error?: string;
29
29
  };
30
+ export type NotificationTarget = {
31
+ type: "channel";
32
+ conversationId: string;
33
+ } | {
34
+ type: "web";
35
+ sessionKey: string;
36
+ };
30
37
  export declare class MulticlawsService extends EventEmitter {
31
38
  private readonly options;
32
39
  private started;
@@ -45,7 +52,7 @@ export declare class MulticlawsService extends EventEmitter {
45
52
  private profileDescription;
46
53
  private readonly gatewayConfig;
47
54
  private readonly resolvedCwd;
48
- private readonly knownChannelIds;
55
+ private readonly notificationTargets;
49
56
  constructor(options: MulticlawsServiceOptions);
50
57
  start(): Promise<void>;
51
58
  stop(): Promise<void>;
@@ -127,8 +134,9 @@ export declare class MulticlawsService extends EventEmitter {
127
134
  /** Fetch with up to 2 retries and exponential backoff. */
128
135
  private fetchWithRetry;
129
136
  /** Register a channel ID for notifications. */
130
- addChannelId(channelId: string): void;
131
- /** Send a notification to all known channels. Individual failures are silently ignored. */
137
+ resolveA2ACallback(taskId: string, result: string): boolean;
138
+ addNotificationTarget(key: string, target: NotificationTarget): void;
139
+ /** Send a notification to all known targets. Individual failures are silently ignored. */
132
140
  private notifyUser;
133
141
  private log;
134
142
  }
@@ -68,7 +68,7 @@ class MulticlawsService extends node_events_1.EventEmitter {
68
68
  profileDescription = "OpenClaw agent";
69
69
  gatewayConfig;
70
70
  resolvedCwd;
71
- knownChannelIds = new Set();
71
+ notificationTargets = new Map();
72
72
  constructor(options) {
73
73
  super();
74
74
  this.options = options;
@@ -123,7 +123,7 @@ class MulticlawsService extends node_events_1.EventEmitter {
123
123
  gatewayConfig: this.options.gatewayConfig ?? null,
124
124
  taskTracker: this.taskTracker,
125
125
  cwd: this.resolvedCwd,
126
- getChannelIds: () => this.knownChannelIds,
126
+ getNotificationTargets: () => this.notificationTargets,
127
127
  logger,
128
128
  });
129
129
  this.agentCard = {
@@ -920,22 +920,34 @@ class MulticlawsService extends node_events_1.EventEmitter {
920
920
  throw lastError;
921
921
  }
922
922
  /** Register a channel ID for notifications. */
923
- addChannelId(channelId) {
924
- if (!this.knownChannelIds.has(channelId)) {
925
- this.knownChannelIds.add(channelId);
926
- this.log("debug", `channel registered: ${channelId} (total: ${this.knownChannelIds.size})`);
923
+ resolveA2ACallback(taskId, result) {
924
+ if (!this.agentExecutor)
925
+ return false;
926
+ return this.agentExecutor.resolveCallback(taskId, result);
927
+ }
928
+ addNotificationTarget(key, target) {
929
+ if (!this.notificationTargets.has(key)) {
930
+ this.notificationTargets.set(key, target);
931
+ this.log("debug", `notification target registered: ${key} (total: ${this.notificationTargets.size})`);
927
932
  }
928
933
  }
929
- /** Send a notification to all known channels. Individual failures are silently ignored. */
934
+ /** Send a notification to all known targets. Individual failures are silently ignored. */
930
935
  async notifyUser(message) {
931
- if (!this.gatewayConfig || this.knownChannelIds.size === 0)
936
+ if (!this.gatewayConfig || this.notificationTargets.size === 0)
932
937
  return;
933
- await Promise.allSettled([...this.knownChannelIds].map((target) => (0, gateway_client_1.invokeGatewayTool)({
934
- gateway: this.gatewayConfig,
935
- tool: "message",
936
- args: { action: "send", target, message },
937
- timeoutMs: 5_000,
938
- })));
938
+ await Promise.allSettled([...this.notificationTargets.values()].map((target) => target.type === "channel"
939
+ ? (0, gateway_client_1.invokeGatewayTool)({
940
+ gateway: this.gatewayConfig,
941
+ tool: "message",
942
+ args: { action: "send", target: target.conversationId, message },
943
+ timeoutMs: 5_000,
944
+ })
945
+ : (0, gateway_client_1.invokeGatewayTool)({
946
+ gateway: this.gatewayConfig,
947
+ tool: "chat.send",
948
+ args: { sessionKey: target.sessionKey, message },
949
+ timeoutMs: 5_000,
950
+ })));
939
951
  }
940
952
  log(level, message) {
941
953
  this.options.logger?.[level]?.(`[multiclaws] ${message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multiclaws",
3
- "version": "0.4.30",
3
+ "version": "0.4.32",
4
4
  "description": "MultiClaws plugin for OpenClaw collaboration via A2A protocol",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,6 +15,12 @@
15
15
  "openclaw.plugin.json",
16
16
  "README.md"
17
17
  ],
18
+ "scripts": {
19
+ "build": "tsc -p tsconfig.json",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "clean": "rm -rf dist"
23
+ },
18
24
  "keywords": [
19
25
  "openclaw",
20
26
  "plugin",
@@ -46,11 +52,5 @@
46
52
  "@types/proper-lockfile": "^4.1.4",
47
53
  "typescript": "^5.9.2",
48
54
  "vitest": "^3.2.4"
49
- },
50
- "scripts": {
51
- "build": "tsc -p tsconfig.json",
52
- "test": "vitest run",
53
- "test:watch": "vitest",
54
- "clean": "rm -rf dist"
55
55
  }
56
- }
56
+ }