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 +62 -5
- package/dist/service/a2a-adapter.d.ts +14 -17
- package/dist/service/a2a-adapter.js +86 -184
- package/dist/service/multiclaws-service.d.ts +11 -3
- package/dist/service/multiclaws-service.js +26 -14
- package/package.json +8 -8
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
|
|
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
|
|
598
|
+
// Collect notification targets from incoming messages (external channels)
|
|
546
599
|
api.on("message_received", (_event, ctx) => {
|
|
547
|
-
if (service && ctx.channelId) {
|
|
548
|
-
service.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
*
|
|
35
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
//
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
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:
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
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
|
-
*
|
|
119
|
-
*
|
|
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
|
-
|
|
180
|
-
const
|
|
181
|
-
if (!
|
|
182
|
-
return
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
/**
|
|
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
|
|
268
|
-
if (!this.gatewayConfig ||
|
|
137
|
+
const targets = this.getNotificationTargets();
|
|
138
|
+
if (!this.gatewayConfig || targets.size === 0)
|
|
269
139
|
return;
|
|
270
|
-
await Promise.allSettled([...
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
924
|
-
if (!this.
|
|
925
|
-
|
|
926
|
-
|
|
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
|
|
934
|
+
/** Send a notification to all known targets. Individual failures are silently ignored. */
|
|
930
935
|
async notifyUser(message) {
|
|
931
|
-
if (!this.gatewayConfig || this.
|
|
936
|
+
if (!this.gatewayConfig || this.notificationTargets.size === 0)
|
|
932
937
|
return;
|
|
933
|
-
await Promise.allSettled([...this.
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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.
|
|
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
|
+
}
|