multiclaws 0.4.42 → 0.4.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/gateway/handlers.d.ts +4 -4
- package/dist/gateway/handlers.js +239 -239
- package/dist/index.d.ts +8 -8
- package/dist/index.js +710 -710
- package/dist/infra/frp.d.ts +55 -55
- package/dist/infra/frp.js +398 -398
- package/dist/infra/gateway-client.d.ts +27 -27
- package/dist/infra/gateway-client.js +136 -136
- package/dist/infra/json-store.d.ts +4 -4
- package/dist/infra/json-store.js +57 -57
- package/dist/infra/logger.d.ts +14 -14
- package/dist/infra/logger.js +25 -25
- package/dist/infra/rate-limiter.d.ts +19 -19
- package/dist/infra/rate-limiter.js +69 -69
- package/dist/infra/tailscale.d.ts +19 -19
- package/dist/infra/tailscale.js +120 -120
- package/dist/infra/telemetry.d.ts +3 -3
- package/dist/infra/telemetry.js +17 -17
- package/dist/infra/version.d.ts +1 -1
- package/dist/infra/version.js +19 -19
- package/dist/service/a2a-adapter.d.ts +80 -80
- package/dist/service/a2a-adapter.js +505 -505
- package/dist/service/agent-profile.d.ts +17 -17
- package/dist/service/agent-profile.js +58 -58
- package/dist/service/agent-registry.d.ts +29 -29
- package/dist/service/agent-registry.js +131 -131
- package/dist/service/multiclaws-service.d.ts +150 -150
- package/dist/service/multiclaws-service.js +1137 -1137
- package/dist/service/session-store.d.ts +46 -46
- package/dist/service/session-store.js +143 -143
- package/dist/task/tracker.d.ts +46 -46
- package/dist/task/tracker.js +191 -191
- package/dist/team/team-store.d.ts +42 -42
- package/dist/team/team-store.js +195 -195
- package/dist/types/openclaw.d.ts +109 -109
- package/dist/types/openclaw.js +2 -2
- package/package.json +1 -1
|
@@ -1,499 +1,499 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.OpenClawAgentExecutor = void 0;
|
|
4
|
-
const gateway_client_1 = require("../infra/gateway-client");
|
|
5
|
-
/* ------------------------------------------------------------------ */
|
|
6
|
-
/* Risk classification */
|
|
7
|
-
/* ------------------------------------------------------------------ */
|
|
8
|
-
/**
|
|
9
|
-
* Heuristic risk classifier. Returns "safe" only when the task is
|
|
10
|
-
* clearly a read-only query; defaults to "risky" for anything ambiguous.
|
|
11
|
-
*
|
|
12
|
-
* This drives the permission gate: risky tasks require explicit human
|
|
13
|
-
* approval before a sub-agent is spawned to execute them.
|
|
14
|
-
*/
|
|
15
|
-
function classifyTaskRisk(taskText) {
|
|
16
|
-
const text = taskText.toLowerCase();
|
|
17
|
-
// Explicit risky patterns (write / modify / execute / send)
|
|
18
|
-
const riskyPatterns = [
|
|
19
|
-
// English — word-boundary matched to avoid false positives
|
|
20
|
-
/\b(write|creat|delet|remov|modif|edit|updat|install|execut|deploy|push|commit|send|post|drop|format|rename|overwrite|reset|wipe|destroy|kill|terminat|rm|mkdir|touch|mv)\b/i,
|
|
21
|
-
// Chinese — multi-character phrases to avoid single-char false positives
|
|
22
|
-
// e.g. 安 alone would match 安排(schedule) or 安全(safe)
|
|
23
|
-
/写入|写文件|写邮件|写信|创建|新建|删除|移除|修改|更改|编辑|更新|升级|安装|部署|执行|运行命令|发送|发邮件|提交|推送|重命名|覆盖|重置|清空|清除|销毁|终止|停止服务|kill进程/,
|
|
24
|
-
];
|
|
25
|
-
// Explicitly safe read-only patterns — checked BEFORE risky to avoid false positives
|
|
26
|
-
// (e.g. "查询并发送报告" is risky overall, but "查询" alone should be safe)
|
|
27
|
-
const safePatterns = [
|
|
28
|
-
// English read-only verbs
|
|
29
|
-
/\b(list|show|get|check|view|read|query|find|search|display|fetch|retriev|look|what|which|count|how many|summariz|describ|explain|analyz|report)\b/i,
|
|
30
|
-
// Chinese read-only verbs (multi-char to be specific)
|
|
31
|
-
/查看|查询|获取|搜索|显示|检查|列出|列举|统计|描述|分析|报告|读取|浏览/,
|
|
32
|
-
// Calendar / scheduling queries
|
|
33
|
-
/\b(calendar|schedule|event|meeting|free|busy|availab|appointment)\b/i,
|
|
34
|
-
/日历|日程|会议|空闲|忙碌|可用时间|时间段|什么时候|哪个时间|安排会议|约会|预约/,
|
|
35
|
-
// Process / system info queries
|
|
36
|
-
/\b(process|pid|cpu|memory|disk|uptime|version|status|running|service|log)\b/i,
|
|
37
|
-
/进程|内存|磁盘|系统状态|运行状态|版本信息|日志|监控/,
|
|
38
|
-
];
|
|
39
|
-
// Safe check first — if clearly a read query, don't let ambiguous chars trigger risky
|
|
40
|
-
if (safePatterns.some((p) => p.test(text))) {
|
|
41
|
-
return "safe";
|
|
42
|
-
}
|
|
43
|
-
if (riskyPatterns.some((p) => p.test(text))) {
|
|
44
|
-
return "risky";
|
|
45
|
-
}
|
|
46
|
-
// Default: treat as risky if uncertain
|
|
47
|
-
return "risky";
|
|
48
|
-
}
|
|
49
|
-
/* ------------------------------------------------------------------ */
|
|
50
|
-
/* Helpers */
|
|
51
|
-
/* ------------------------------------------------------------------ */
|
|
52
|
-
function extractTextFromMessage(message) {
|
|
53
|
-
if (!message.parts)
|
|
54
|
-
return "";
|
|
55
|
-
return message.parts
|
|
56
|
-
.filter((p) => p.kind === "text")
|
|
57
|
-
.map((p) => p.text)
|
|
58
|
-
.join("\n");
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Bridges the A2A protocol to OpenClaw's session injection mechanism.
|
|
62
|
-
*
|
|
63
|
-
* When a remote agent sends a task via A2A `message/send`,
|
|
64
|
-
* this executor:
|
|
65
|
-
* 1. Classifies the task risk (safe vs risky)
|
|
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
|
-
* 6. Returns the final result as a Message
|
|
72
|
-
*/
|
|
73
|
-
class OpenClawAgentExecutor {
|
|
74
|
-
gatewayConfig;
|
|
75
|
-
taskTracker;
|
|
76
|
-
getNotificationTargets;
|
|
77
|
-
registerDiscoveredTarget;
|
|
78
|
-
logger;
|
|
79
|
-
cwd;
|
|
80
|
-
pendingCallbacks = new Map();
|
|
81
|
-
pendingApprovals = new Map();
|
|
82
|
-
constructor(options) {
|
|
83
|
-
this.gatewayConfig = options.gatewayConfig;
|
|
84
|
-
this.taskTracker = options.taskTracker;
|
|
85
|
-
this.getNotificationTargets = options.getNotificationTargets ?? (() => new Map());
|
|
86
|
-
this.registerDiscoveredTarget = options.registerDiscoveredTarget;
|
|
87
|
-
this.logger = options.logger;
|
|
88
|
-
this.cwd = options.cwd || process.cwd();
|
|
89
|
-
}
|
|
90
|
-
async execute(context, eventBus) {
|
|
91
|
-
const taskText = extractTextFromMessage(context.userMessage);
|
|
92
|
-
const taskId = context.taskId;
|
|
93
|
-
this.logger.info(`[a2a-adapter] ▶ execute() called — taskId=${taskId}, textLen=${taskText.length}`);
|
|
94
|
-
if (!taskText.trim()) {
|
|
95
|
-
this.logger.warn(`[a2a-adapter] ✗ empty task text, rejecting — taskId=${taskId}`);
|
|
96
|
-
this.publishMessage(eventBus, "Error: empty task received.");
|
|
97
|
-
eventBus.finished();
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const meta = context.userMessage.metadata ?? {};
|
|
101
|
-
const fromAgentUrl = meta.agentUrl ?? "unknown";
|
|
102
|
-
const fromAgentName = meta.agentName || fromAgentUrl;
|
|
103
|
-
this.logger.info(`[a2a-adapter] task ${taskId} from ${fromAgentName} (${fromAgentUrl}): ${taskText.slice(0, 120)}`);
|
|
104
|
-
this.taskTracker.create({
|
|
105
|
-
fromPeerId: fromAgentUrl,
|
|
106
|
-
toPeerId: "local",
|
|
107
|
-
task: taskText,
|
|
108
|
-
});
|
|
109
|
-
this.logger.info(`[a2a-adapter] task ${taskId} tracked`);
|
|
110
|
-
if (!this.gatewayConfig) {
|
|
111
|
-
this.logger.error(`[a2a-adapter] ✗ gateway config not available — taskId=${taskId}`);
|
|
112
|
-
this.taskTracker.update(taskId, { status: "failed", error: "gateway config not available" });
|
|
113
|
-
this.publishMessage(eventBus, "Error: gateway config not available, cannot execute task.");
|
|
114
|
-
eventBus.finished();
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
// ── Step 1: Risk classification ──
|
|
118
|
-
const risk = classifyTaskRisk(taskText);
|
|
119
|
-
this.logger.info(`[a2a-adapter] task ${taskId} [step:risk-classify] risk=${risk}, text="${taskText.slice(0, 60)}"`);
|
|
120
|
-
if (risk === "risky") {
|
|
121
|
-
// ── Step 2a: Approval gate (risky tasks only) ──
|
|
122
|
-
const approvalTimeoutMs = 5 * 60 * 1000; // 5 minutes
|
|
123
|
-
const approvalPromise = this.createApprovalCallback(taskId, approvalTimeoutMs);
|
|
124
|
-
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-request] sending approval request to user (timeout=${approvalTimeoutMs / 1000}s)`);
|
|
125
|
-
void this.notifyUser(buildApprovalRequest(taskId, fromAgentName, taskText));
|
|
126
|
-
let approved;
|
|
127
|
-
try {
|
|
128
|
-
approved = await approvalPromise;
|
|
129
|
-
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-result] user responded: approved=${approved}`);
|
|
130
|
-
}
|
|
131
|
-
catch (err) {
|
|
132
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
133
|
-
const isCanceled = err instanceof Error && err.message === "canceled";
|
|
134
|
-
if (isCanceled) {
|
|
135
|
-
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-result] caught "canceled" error → aborting task`);
|
|
136
|
-
this.taskTracker.update(taskId, { status: "failed", error: "canceled" });
|
|
137
|
-
this.publishMessage(eventBus, "Task was canceled.");
|
|
138
|
-
eventBus.finished();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
// Approval timed out → auto-reject
|
|
142
|
-
approved = false;
|
|
143
|
-
this.logger.warn(`[a2a-adapter] task ${taskId} [step:approval-result] caught error: ${errMsg} → treating as auto-reject`);
|
|
144
|
-
}
|
|
145
|
-
if (!approved) {
|
|
146
|
-
const reason = "用户拒绝或未在超时时间内授权。";
|
|
147
|
-
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-rejected] → aborting task, reason: ${reason}`);
|
|
148
|
-
this.taskTracker.update(taskId, { status: "failed", error: reason });
|
|
149
|
-
this.publishMessage(eventBus, `任务已被拒绝:${reason}`);
|
|
150
|
-
eventBus.finished();
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-passed] → proceeding to find target session`);
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
this.logger.info(`[a2a-adapter] task ${taskId} [step:auto-execute] safe query, skipping approval → proceeding to find target session`);
|
|
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}`);
|
|
170
|
-
try {
|
|
171
|
-
// ── Step 4: Register callback ──
|
|
172
|
-
const timeoutMs = 180_000;
|
|
173
|
-
const resultPromise = this.createCallback(taskId, timeoutMs);
|
|
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)({
|
|
179
|
-
gateway: this.gatewayConfig,
|
|
180
|
-
tool: "sessions_send",
|
|
181
|
-
args: { sessionKey: targetSessionKey, message: prompt },
|
|
182
|
-
timeoutMs: 15_000,
|
|
183
|
-
});
|
|
184
|
-
this.logger.info(`[a2a-adapter] task ${taskId} [step:inject-task] ✓ sessions_send succeeded → waiting for callback...`);
|
|
185
|
-
// ── Step 6: Wait for callback ──
|
|
186
|
-
const output = await resultPromise;
|
|
187
|
-
// ── Step 7: Return result ──
|
|
188
|
-
this.taskTracker.update(taskId, { status: "completed", result: output });
|
|
189
|
-
this.logger.info(`[a2a-adapter] task ${taskId} [step:completed] ✓ resultLen=${output.length}, preview="${output.slice(0, 120)}"`);
|
|
190
|
-
this.publishMessage(eventBus, output || "Task completed with no output.");
|
|
191
|
-
}
|
|
192
|
-
catch (err) {
|
|
193
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
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`);
|
|
198
|
-
this.taskTracker.update(taskId, { status: "failed", error: errorMsg });
|
|
199
|
-
void this.notifyUser(`❌ 来自 **${fromAgentName}** 的任务执行失败:${errorMsg}`);
|
|
200
|
-
this.publishMessage(eventBus, `Error: ${errorMsg}`);
|
|
201
|
-
}
|
|
202
|
-
this.logger.info(`[a2a-adapter] task ${taskId} [step:finished] eventBus.finished()`);
|
|
203
|
-
eventBus.finished();
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Called by the `multiclaws_a2a_callback` tool when a sub-agent reports its result.
|
|
207
|
-
* Returns true if a pending callback was found and resolved.
|
|
208
|
-
*/
|
|
209
|
-
resolveCallback(taskId, result) {
|
|
210
|
-
const pending = this.pendingCallbacks.get(taskId);
|
|
211
|
-
if (!pending) {
|
|
212
|
-
this.logger.warn(`[a2a-adapter] resolveCallback: no pending callback for taskId=${taskId} (may have timed out)`);
|
|
213
|
-
return false;
|
|
214
|
-
}
|
|
215
|
-
clearTimeout(pending.timer);
|
|
216
|
-
this.pendingCallbacks.delete(taskId);
|
|
217
|
-
this.logger.info(`[a2a-adapter] resolveCallback: taskId=${taskId} resolved — resultLen=${result.length}`);
|
|
218
|
-
pending.resolve(result);
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Called when the local human owner approves or rejects a pending risky task.
|
|
223
|
-
* Returns true if a pending approval was found.
|
|
224
|
-
*/
|
|
225
|
-
resolveApproval(taskId, approved) {
|
|
226
|
-
const pending = this.pendingApprovals.get(taskId);
|
|
227
|
-
if (!pending) {
|
|
228
|
-
this.logger.warn(`[a2a-adapter] resolveApproval: no pending approval for taskId=${taskId}`);
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
clearTimeout(pending.timer);
|
|
232
|
-
this.pendingApprovals.delete(taskId);
|
|
233
|
-
this.logger.info(`[a2a-adapter] resolveApproval: taskId=${taskId} approved=${approved}`);
|
|
234
|
-
pending.resolve(approved);
|
|
235
|
-
return true;
|
|
236
|
-
}
|
|
237
|
-
async cancelTask(taskId, eventBus) {
|
|
238
|
-
this.logger.info(`[a2a-adapter] cancelTask(taskId=${taskId})`);
|
|
239
|
-
// Reject pending approval if any — distinct from user-rejection, uses Error("canceled")
|
|
240
|
-
const approval = this.pendingApprovals.get(taskId);
|
|
241
|
-
if (approval) {
|
|
242
|
-
clearTimeout(approval.timer);
|
|
243
|
-
this.pendingApprovals.delete(taskId);
|
|
244
|
-
approval.reject(new Error("canceled"));
|
|
245
|
-
this.logger.info(`[a2a-adapter] cancelTask: pending approval canceled for taskId=${taskId}`);
|
|
246
|
-
}
|
|
247
|
-
// Reject pending callback if any
|
|
248
|
-
const pending = this.pendingCallbacks.get(taskId);
|
|
249
|
-
if (pending) {
|
|
250
|
-
clearTimeout(pending.timer);
|
|
251
|
-
this.pendingCallbacks.delete(taskId);
|
|
252
|
-
pending.reject(new Error("canceled"));
|
|
253
|
-
this.logger.info(`[a2a-adapter] cancelTask: pending callback rejected for taskId=${taskId}`);
|
|
254
|
-
}
|
|
255
|
-
this.taskTracker.update(taskId, { status: "failed", error: "canceled" });
|
|
256
|
-
this.publishMessage(eventBus, "Task was canceled.");
|
|
257
|
-
eventBus.finished();
|
|
258
|
-
}
|
|
259
|
-
updateGatewayConfig(config) {
|
|
260
|
-
this.gatewayConfig = config;
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Create a pending callback that resolves when the sub-agent reports back,
|
|
264
|
-
* or rejects on timeout.
|
|
265
|
-
*/
|
|
266
|
-
createCallback(taskId, timeoutMs) {
|
|
267
|
-
return new Promise((resolve, reject) => {
|
|
268
|
-
const timer = setTimeout(() => {
|
|
269
|
-
this.pendingCallbacks.delete(taskId);
|
|
270
|
-
this.logger.error(`[a2a-adapter] ✗ task ${taskId} callback timed out after ${timeoutMs / 1000}s — pending callbacks remaining: ${this.pendingCallbacks.size}`);
|
|
271
|
-
reject(new Error(`task timed out after ${timeoutMs / 1000}s waiting for sub-agent callback`));
|
|
272
|
-
}, timeoutMs);
|
|
273
|
-
this.pendingCallbacks.set(taskId, { resolve, reject, timer });
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Create a pending approval that resolves when the human owner responds,
|
|
278
|
-
* or rejects on timeout or cancellation.
|
|
279
|
-
*/
|
|
280
|
-
createApprovalCallback(taskId, timeoutMs) {
|
|
281
|
-
return new Promise((resolve, reject) => {
|
|
282
|
-
const timer = setTimeout(() => {
|
|
283
|
-
this.pendingApprovals.delete(taskId);
|
|
284
|
-
this.logger.warn(`[a2a-adapter] task ${taskId} approval timed out after ${timeoutMs / 1000}s`);
|
|
285
|
-
reject(new Error(`approval timed out after ${timeoutMs / 1000}s`));
|
|
286
|
-
}, timeoutMs);
|
|
287
|
-
this.pendingApprovals.set(taskId, { resolve, reject, timer });
|
|
288
|
-
});
|
|
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
|
-
}
|
|
353
|
-
/**
|
|
354
|
-
* Discover the most recently active non-internal session via sessions_list.
|
|
355
|
-
* Used as fallback when no notification targets have been registered yet
|
|
356
|
-
* (e.g. right after a gateway restart before the user sends their first message).
|
|
357
|
-
*/
|
|
358
|
-
async discoverActiveSession() {
|
|
359
|
-
if (!this.gatewayConfig) {
|
|
360
|
-
this.logger.warn(`[a2a-adapter] discoverActiveSession: skipped — no gateway config`);
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
try {
|
|
364
|
-
this.logger.info(`[a2a-adapter] discoverActiveSession: calling sessions_list (limit=10, activeMinutes=120)`);
|
|
365
|
-
const raw = await (0, gateway_client_1.invokeGatewayTool)({
|
|
366
|
-
gateway: this.gatewayConfig,
|
|
367
|
-
tool: "sessions_list",
|
|
368
|
-
args: { limit: 10, activeMinutes: 120 },
|
|
369
|
-
timeoutMs: 5_000,
|
|
370
|
-
});
|
|
371
|
-
this.logger.info(`[a2a-adapter] discoverActiveSession: raw result = ${JSON.stringify(raw).slice(0, 500)}`);
|
|
372
|
-
// Unwrap gateway tool standard response: { content: [{ type: "text", text: "..." }] }
|
|
373
|
-
let parsed = raw;
|
|
374
|
-
if (raw?.content?.[0]?.type === "text") {
|
|
375
|
-
try {
|
|
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`);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
const sessions = parsed?.sessions ?? [];
|
|
384
|
-
this.logger.info(`[a2a-adapter] discoverActiveSession: found ${sessions.length} sessions`);
|
|
385
|
-
const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
|
|
386
|
-
// sessions_list returns "key" not "sessionKey"
|
|
387
|
-
const session = sessions.find((s) => {
|
|
388
|
-
const k = (s.key ?? s.sessionKey);
|
|
389
|
-
return k && !INTERNAL_PREFIXES.some((p) => k.startsWith(p));
|
|
390
|
-
});
|
|
391
|
-
const matchedKey = (session?.key ?? session?.sessionKey);
|
|
392
|
-
if (matchedKey) {
|
|
393
|
-
this.logger.info(`[a2a-adapter] discoverActiveSession: ✓ matched session ${matchedKey}`);
|
|
394
|
-
}
|
|
395
|
-
else {
|
|
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)"}`));
|
|
398
|
-
}
|
|
399
|
-
return matchedKey ?? null;
|
|
400
|
-
}
|
|
401
|
-
catch (err) {
|
|
402
|
-
this.logger.error(`[a2a-adapter] discoverActiveSession: ✗ caught error — ${err instanceof Error ? err.message : String(err)}, returning null`);
|
|
403
|
-
return null;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
/** Send a notification to all known targets. Individual failures are silently ignored. */
|
|
407
|
-
async notifyUser(message) {
|
|
408
|
-
const targets = this.getNotificationTargets();
|
|
409
|
-
this.logger.info(`[a2a-adapter] notifyUser: targets=${targets.size}, msgLen=${message.length}, preview="${message.slice(0, 80)}"`);
|
|
410
|
-
if (!this.gatewayConfig) {
|
|
411
|
-
this.logger.warn(`[a2a-adapter] notifyUser: skipped — no gateway config, message lost`);
|
|
412
|
-
return;
|
|
413
|
-
}
|
|
414
|
-
// Fallback: no registered targets yet (e.g. right after gateway restart).
|
|
415
|
-
// Discover the active session and send directly via sessions_send.
|
|
416
|
-
if (targets.size === 0) {
|
|
417
|
-
this.logger.info(`[a2a-adapter] notifyUser: no registered targets → falling back to findTargetSession()`);
|
|
418
|
-
const sessionKey = await this.findTargetSession();
|
|
419
|
-
if (sessionKey) {
|
|
420
|
-
this.logger.info(`[a2a-adapter] notifyUser: fallback discovered session ${sessionKey} → calling sessions_send`);
|
|
421
|
-
try {
|
|
422
|
-
await (0, gateway_client_1.invokeGatewayTool)({
|
|
423
|
-
gateway: this.gatewayConfig,
|
|
424
|
-
tool: "sessions_send",
|
|
425
|
-
args: { sessionKey, message },
|
|
426
|
-
timeoutMs: 5_000,
|
|
427
|
-
});
|
|
428
|
-
this.logger.info(`[a2a-adapter] notifyUser: ✓ fallback sessions_send to ${sessionKey} succeeded`);
|
|
429
|
-
// Also register this session for future notifications
|
|
430
|
-
if (this.registerDiscoveredTarget) {
|
|
431
|
-
this.registerDiscoveredTarget(sessionKey);
|
|
432
|
-
this.logger.info(`[a2a-adapter] notifyUser: registered ${sessionKey} as notification target for future use`);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
catch (err) {
|
|
436
|
-
this.logger.error(`[a2a-adapter] notifyUser: ✗ fallback sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
else {
|
|
440
|
-
this.logger.warn(`[a2a-adapter] notifyUser: ✗ findTargetSession returned null — no active session found, message lost`);
|
|
441
|
-
}
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
this.logger.info(`[a2a-adapter] notifyUser: sending to ${targets.size} registered target(s): [${[...targets.keys()].join(", ")}]`);
|
|
445
|
-
const results = await Promise.allSettled([...targets.entries()].map(async ([key, target]) => {
|
|
446
|
-
this.logger.info(`[a2a-adapter] notifyUser: → ${key} (type=${target.type})`);
|
|
447
|
-
try {
|
|
448
|
-
await (target.type === "channel"
|
|
449
|
-
? (0, gateway_client_1.invokeGatewayTool)({
|
|
450
|
-
gateway: this.gatewayConfig,
|
|
451
|
-
tool: "message",
|
|
452
|
-
args: { action: "send", target: target.conversationId, message },
|
|
453
|
-
timeoutMs: 5_000,
|
|
454
|
-
})
|
|
455
|
-
: (0, gateway_client_1.invokeGatewayTool)({
|
|
456
|
-
gateway: this.gatewayConfig,
|
|
457
|
-
tool: "sessions_send",
|
|
458
|
-
args: { sessionKey: target.sessionKey, message },
|
|
459
|
-
timeoutMs: 5_000,
|
|
460
|
-
}));
|
|
461
|
-
this.logger.info(`[a2a-adapter] notifyUser: ✓ ${key} (${target.type}) succeeded`);
|
|
462
|
-
}
|
|
463
|
-
catch (err) {
|
|
464
|
-
this.logger.error(`[a2a-adapter] notifyUser: ✗ ${key} (${target.type}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
465
|
-
throw err;
|
|
466
|
-
}
|
|
467
|
-
}));
|
|
468
|
-
const ok = results.filter((r) => r.status === "fulfilled").length;
|
|
469
|
-
const fail = results.filter((r) => r.status === "rejected").length;
|
|
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
|
-
}
|
|
476
|
-
}
|
|
477
|
-
publishMessage(eventBus, text) {
|
|
478
|
-
const message = {
|
|
479
|
-
kind: "message",
|
|
480
|
-
role: "agent",
|
|
481
|
-
messageId: `msg-${Date.now()}`,
|
|
482
|
-
parts: [{ kind: "text", text }],
|
|
483
|
-
};
|
|
484
|
-
eventBus.publish(message);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
exports.OpenClawAgentExecutor = OpenClawAgentExecutor;
|
|
488
|
-
/* ------------------------------------------------------------------ */
|
|
489
|
-
/* Prompt builders */
|
|
490
|
-
/* ------------------------------------------------------------------ */
|
|
491
|
-
/**
|
|
492
|
-
* Build the approval request message injected into the human's active session.
|
|
493
|
-
* The AI in that session will relay it and handle the human's approve/reject response.
|
|
494
|
-
*/
|
|
495
|
-
function buildApprovalRequest(taskId, fromAgentName, taskText) {
|
|
496
|
-
const preview = taskText.length > 600 ? taskText.slice(0, 600) + "…" : taskText;
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OpenClawAgentExecutor = void 0;
|
|
4
|
+
const gateway_client_1 = require("../infra/gateway-client");
|
|
5
|
+
/* ------------------------------------------------------------------ */
|
|
6
|
+
/* Risk classification */
|
|
7
|
+
/* ------------------------------------------------------------------ */
|
|
8
|
+
/**
|
|
9
|
+
* Heuristic risk classifier. Returns "safe" only when the task is
|
|
10
|
+
* clearly a read-only query; defaults to "risky" for anything ambiguous.
|
|
11
|
+
*
|
|
12
|
+
* This drives the permission gate: risky tasks require explicit human
|
|
13
|
+
* approval before a sub-agent is spawned to execute them.
|
|
14
|
+
*/
|
|
15
|
+
function classifyTaskRisk(taskText) {
|
|
16
|
+
const text = taskText.toLowerCase();
|
|
17
|
+
// Explicit risky patterns (write / modify / execute / send)
|
|
18
|
+
const riskyPatterns = [
|
|
19
|
+
// English — word-boundary matched to avoid false positives
|
|
20
|
+
/\b(write|creat|delet|remov|modif|edit|updat|install|execut|deploy|push|commit|send|post|drop|format|rename|overwrite|reset|wipe|destroy|kill|terminat|rm|mkdir|touch|mv)\b/i,
|
|
21
|
+
// Chinese — multi-character phrases to avoid single-char false positives
|
|
22
|
+
// e.g. 安 alone would match 安排(schedule) or 安全(safe)
|
|
23
|
+
/写入|写文件|写邮件|写信|创建|新建|删除|移除|修改|更改|编辑|更新|升级|安装|部署|执行|运行命令|发送|发邮件|提交|推送|重命名|覆盖|重置|清空|清除|销毁|终止|停止服务|kill进程/,
|
|
24
|
+
];
|
|
25
|
+
// Explicitly safe read-only patterns — checked BEFORE risky to avoid false positives
|
|
26
|
+
// (e.g. "查询并发送报告" is risky overall, but "查询" alone should be safe)
|
|
27
|
+
const safePatterns = [
|
|
28
|
+
// English read-only verbs
|
|
29
|
+
/\b(list|show|get|check|view|read|query|find|search|display|fetch|retriev|look|what|which|count|how many|summariz|describ|explain|analyz|report)\b/i,
|
|
30
|
+
// Chinese read-only verbs (multi-char to be specific)
|
|
31
|
+
/查看|查询|获取|搜索|显示|检查|列出|列举|统计|描述|分析|报告|读取|浏览/,
|
|
32
|
+
// Calendar / scheduling queries
|
|
33
|
+
/\b(calendar|schedule|event|meeting|free|busy|availab|appointment)\b/i,
|
|
34
|
+
/日历|日程|会议|空闲|忙碌|可用时间|时间段|什么时候|哪个时间|安排会议|约会|预约/,
|
|
35
|
+
// Process / system info queries
|
|
36
|
+
/\b(process|pid|cpu|memory|disk|uptime|version|status|running|service|log)\b/i,
|
|
37
|
+
/进程|内存|磁盘|系统状态|运行状态|版本信息|日志|监控/,
|
|
38
|
+
];
|
|
39
|
+
// Safe check first — if clearly a read query, don't let ambiguous chars trigger risky
|
|
40
|
+
if (safePatterns.some((p) => p.test(text))) {
|
|
41
|
+
return "safe";
|
|
42
|
+
}
|
|
43
|
+
if (riskyPatterns.some((p) => p.test(text))) {
|
|
44
|
+
return "risky";
|
|
45
|
+
}
|
|
46
|
+
// Default: treat as risky if uncertain
|
|
47
|
+
return "risky";
|
|
48
|
+
}
|
|
49
|
+
/* ------------------------------------------------------------------ */
|
|
50
|
+
/* Helpers */
|
|
51
|
+
/* ------------------------------------------------------------------ */
|
|
52
|
+
function extractTextFromMessage(message) {
|
|
53
|
+
if (!message.parts)
|
|
54
|
+
return "";
|
|
55
|
+
return message.parts
|
|
56
|
+
.filter((p) => p.kind === "text")
|
|
57
|
+
.map((p) => p.text)
|
|
58
|
+
.join("\n");
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Bridges the A2A protocol to OpenClaw's session injection mechanism.
|
|
62
|
+
*
|
|
63
|
+
* When a remote agent sends a task via A2A `message/send`,
|
|
64
|
+
* this executor:
|
|
65
|
+
* 1. Classifies the task risk (safe vs risky)
|
|
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
|
+
* 6. Returns the final result as a Message
|
|
72
|
+
*/
|
|
73
|
+
class OpenClawAgentExecutor {
|
|
74
|
+
gatewayConfig;
|
|
75
|
+
taskTracker;
|
|
76
|
+
getNotificationTargets;
|
|
77
|
+
registerDiscoveredTarget;
|
|
78
|
+
logger;
|
|
79
|
+
cwd;
|
|
80
|
+
pendingCallbacks = new Map();
|
|
81
|
+
pendingApprovals = new Map();
|
|
82
|
+
constructor(options) {
|
|
83
|
+
this.gatewayConfig = options.gatewayConfig;
|
|
84
|
+
this.taskTracker = options.taskTracker;
|
|
85
|
+
this.getNotificationTargets = options.getNotificationTargets ?? (() => new Map());
|
|
86
|
+
this.registerDiscoveredTarget = options.registerDiscoveredTarget;
|
|
87
|
+
this.logger = options.logger;
|
|
88
|
+
this.cwd = options.cwd || process.cwd();
|
|
89
|
+
}
|
|
90
|
+
async execute(context, eventBus) {
|
|
91
|
+
const taskText = extractTextFromMessage(context.userMessage);
|
|
92
|
+
const taskId = context.taskId;
|
|
93
|
+
this.logger.info(`[a2a-adapter] ▶ execute() called — taskId=${taskId}, textLen=${taskText.length}`);
|
|
94
|
+
if (!taskText.trim()) {
|
|
95
|
+
this.logger.warn(`[a2a-adapter] ✗ empty task text, rejecting — taskId=${taskId}`);
|
|
96
|
+
this.publishMessage(eventBus, "Error: empty task received.");
|
|
97
|
+
eventBus.finished();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const meta = context.userMessage.metadata ?? {};
|
|
101
|
+
const fromAgentUrl = meta.agentUrl ?? "unknown";
|
|
102
|
+
const fromAgentName = meta.agentName || fromAgentUrl;
|
|
103
|
+
this.logger.info(`[a2a-adapter] task ${taskId} from ${fromAgentName} (${fromAgentUrl}): ${taskText.slice(0, 120)}`);
|
|
104
|
+
this.taskTracker.create({
|
|
105
|
+
fromPeerId: fromAgentUrl,
|
|
106
|
+
toPeerId: "local",
|
|
107
|
+
task: taskText,
|
|
108
|
+
});
|
|
109
|
+
this.logger.info(`[a2a-adapter] task ${taskId} tracked`);
|
|
110
|
+
if (!this.gatewayConfig) {
|
|
111
|
+
this.logger.error(`[a2a-adapter] ✗ gateway config not available — taskId=${taskId}`);
|
|
112
|
+
this.taskTracker.update(taskId, { status: "failed", error: "gateway config not available" });
|
|
113
|
+
this.publishMessage(eventBus, "Error: gateway config not available, cannot execute task.");
|
|
114
|
+
eventBus.finished();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// ── Step 1: Risk classification ──
|
|
118
|
+
const risk = classifyTaskRisk(taskText);
|
|
119
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:risk-classify] risk=${risk}, text="${taskText.slice(0, 60)}"`);
|
|
120
|
+
if (risk === "risky") {
|
|
121
|
+
// ── Step 2a: Approval gate (risky tasks only) ──
|
|
122
|
+
const approvalTimeoutMs = 5 * 60 * 1000; // 5 minutes
|
|
123
|
+
const approvalPromise = this.createApprovalCallback(taskId, approvalTimeoutMs);
|
|
124
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-request] sending approval request to user (timeout=${approvalTimeoutMs / 1000}s)`);
|
|
125
|
+
void this.notifyUser(buildApprovalRequest(taskId, fromAgentName, taskText));
|
|
126
|
+
let approved;
|
|
127
|
+
try {
|
|
128
|
+
approved = await approvalPromise;
|
|
129
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-result] user responded: approved=${approved}`);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
133
|
+
const isCanceled = err instanceof Error && err.message === "canceled";
|
|
134
|
+
if (isCanceled) {
|
|
135
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-result] caught "canceled" error → aborting task`);
|
|
136
|
+
this.taskTracker.update(taskId, { status: "failed", error: "canceled" });
|
|
137
|
+
this.publishMessage(eventBus, "Task was canceled.");
|
|
138
|
+
eventBus.finished();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Approval timed out → auto-reject
|
|
142
|
+
approved = false;
|
|
143
|
+
this.logger.warn(`[a2a-adapter] task ${taskId} [step:approval-result] caught error: ${errMsg} → treating as auto-reject`);
|
|
144
|
+
}
|
|
145
|
+
if (!approved) {
|
|
146
|
+
const reason = "用户拒绝或未在超时时间内授权。";
|
|
147
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-rejected] → aborting task, reason: ${reason}`);
|
|
148
|
+
this.taskTracker.update(taskId, { status: "failed", error: reason });
|
|
149
|
+
this.publishMessage(eventBus, `任务已被拒绝:${reason}`);
|
|
150
|
+
eventBus.finished();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-passed] → proceeding to find target session`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:auto-execute] safe query, skipping approval → proceeding to find target session`);
|
|
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}`);
|
|
170
|
+
try {
|
|
171
|
+
// ── Step 4: Register callback ──
|
|
172
|
+
const timeoutMs = 180_000;
|
|
173
|
+
const resultPromise = this.createCallback(taskId, timeoutMs);
|
|
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)({
|
|
179
|
+
gateway: this.gatewayConfig,
|
|
180
|
+
tool: "sessions_send",
|
|
181
|
+
args: { sessionKey: targetSessionKey, message: prompt },
|
|
182
|
+
timeoutMs: 15_000,
|
|
183
|
+
});
|
|
184
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:inject-task] ✓ sessions_send succeeded → waiting for callback...`);
|
|
185
|
+
// ── Step 6: Wait for callback ──
|
|
186
|
+
const output = await resultPromise;
|
|
187
|
+
// ── Step 7: Return result ──
|
|
188
|
+
this.taskTracker.update(taskId, { status: "completed", result: output });
|
|
189
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:completed] ✓ resultLen=${output.length}, preview="${output.slice(0, 120)}"`);
|
|
190
|
+
this.publishMessage(eventBus, output || "Task completed with no output.");
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
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`);
|
|
198
|
+
this.taskTracker.update(taskId, { status: "failed", error: errorMsg });
|
|
199
|
+
void this.notifyUser(`❌ 来自 **${fromAgentName}** 的任务执行失败:${errorMsg}`);
|
|
200
|
+
this.publishMessage(eventBus, `Error: ${errorMsg}`);
|
|
201
|
+
}
|
|
202
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:finished] eventBus.finished()`);
|
|
203
|
+
eventBus.finished();
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Called by the `multiclaws_a2a_callback` tool when a sub-agent reports its result.
|
|
207
|
+
* Returns true if a pending callback was found and resolved.
|
|
208
|
+
*/
|
|
209
|
+
resolveCallback(taskId, result) {
|
|
210
|
+
const pending = this.pendingCallbacks.get(taskId);
|
|
211
|
+
if (!pending) {
|
|
212
|
+
this.logger.warn(`[a2a-adapter] resolveCallback: no pending callback for taskId=${taskId} (may have timed out)`);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
clearTimeout(pending.timer);
|
|
216
|
+
this.pendingCallbacks.delete(taskId);
|
|
217
|
+
this.logger.info(`[a2a-adapter] resolveCallback: taskId=${taskId} resolved — resultLen=${result.length}`);
|
|
218
|
+
pending.resolve(result);
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Called when the local human owner approves or rejects a pending risky task.
|
|
223
|
+
* Returns true if a pending approval was found.
|
|
224
|
+
*/
|
|
225
|
+
resolveApproval(taskId, approved) {
|
|
226
|
+
const pending = this.pendingApprovals.get(taskId);
|
|
227
|
+
if (!pending) {
|
|
228
|
+
this.logger.warn(`[a2a-adapter] resolveApproval: no pending approval for taskId=${taskId}`);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
clearTimeout(pending.timer);
|
|
232
|
+
this.pendingApprovals.delete(taskId);
|
|
233
|
+
this.logger.info(`[a2a-adapter] resolveApproval: taskId=${taskId} approved=${approved}`);
|
|
234
|
+
pending.resolve(approved);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
async cancelTask(taskId, eventBus) {
|
|
238
|
+
this.logger.info(`[a2a-adapter] cancelTask(taskId=${taskId})`);
|
|
239
|
+
// Reject pending approval if any — distinct from user-rejection, uses Error("canceled")
|
|
240
|
+
const approval = this.pendingApprovals.get(taskId);
|
|
241
|
+
if (approval) {
|
|
242
|
+
clearTimeout(approval.timer);
|
|
243
|
+
this.pendingApprovals.delete(taskId);
|
|
244
|
+
approval.reject(new Error("canceled"));
|
|
245
|
+
this.logger.info(`[a2a-adapter] cancelTask: pending approval canceled for taskId=${taskId}`);
|
|
246
|
+
}
|
|
247
|
+
// Reject pending callback if any
|
|
248
|
+
const pending = this.pendingCallbacks.get(taskId);
|
|
249
|
+
if (pending) {
|
|
250
|
+
clearTimeout(pending.timer);
|
|
251
|
+
this.pendingCallbacks.delete(taskId);
|
|
252
|
+
pending.reject(new Error("canceled"));
|
|
253
|
+
this.logger.info(`[a2a-adapter] cancelTask: pending callback rejected for taskId=${taskId}`);
|
|
254
|
+
}
|
|
255
|
+
this.taskTracker.update(taskId, { status: "failed", error: "canceled" });
|
|
256
|
+
this.publishMessage(eventBus, "Task was canceled.");
|
|
257
|
+
eventBus.finished();
|
|
258
|
+
}
|
|
259
|
+
updateGatewayConfig(config) {
|
|
260
|
+
this.gatewayConfig = config;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Create a pending callback that resolves when the sub-agent reports back,
|
|
264
|
+
* or rejects on timeout.
|
|
265
|
+
*/
|
|
266
|
+
createCallback(taskId, timeoutMs) {
|
|
267
|
+
return new Promise((resolve, reject) => {
|
|
268
|
+
const timer = setTimeout(() => {
|
|
269
|
+
this.pendingCallbacks.delete(taskId);
|
|
270
|
+
this.logger.error(`[a2a-adapter] ✗ task ${taskId} callback timed out after ${timeoutMs / 1000}s — pending callbacks remaining: ${this.pendingCallbacks.size}`);
|
|
271
|
+
reject(new Error(`task timed out after ${timeoutMs / 1000}s waiting for sub-agent callback`));
|
|
272
|
+
}, timeoutMs);
|
|
273
|
+
this.pendingCallbacks.set(taskId, { resolve, reject, timer });
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Create a pending approval that resolves when the human owner responds,
|
|
278
|
+
* or rejects on timeout or cancellation.
|
|
279
|
+
*/
|
|
280
|
+
createApprovalCallback(taskId, timeoutMs) {
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
const timer = setTimeout(() => {
|
|
283
|
+
this.pendingApprovals.delete(taskId);
|
|
284
|
+
this.logger.warn(`[a2a-adapter] task ${taskId} approval timed out after ${timeoutMs / 1000}s`);
|
|
285
|
+
reject(new Error(`approval timed out after ${timeoutMs / 1000}s`));
|
|
286
|
+
}, timeoutMs);
|
|
287
|
+
this.pendingApprovals.set(taskId, { resolve, reject, timer });
|
|
288
|
+
});
|
|
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
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Discover the most recently active non-internal session via sessions_list.
|
|
355
|
+
* Used as fallback when no notification targets have been registered yet
|
|
356
|
+
* (e.g. right after a gateway restart before the user sends their first message).
|
|
357
|
+
*/
|
|
358
|
+
async discoverActiveSession() {
|
|
359
|
+
if (!this.gatewayConfig) {
|
|
360
|
+
this.logger.warn(`[a2a-adapter] discoverActiveSession: skipped — no gateway config`);
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
this.logger.info(`[a2a-adapter] discoverActiveSession: calling sessions_list (limit=10, activeMinutes=120)`);
|
|
365
|
+
const raw = await (0, gateway_client_1.invokeGatewayTool)({
|
|
366
|
+
gateway: this.gatewayConfig,
|
|
367
|
+
tool: "sessions_list",
|
|
368
|
+
args: { limit: 10, activeMinutes: 120 },
|
|
369
|
+
timeoutMs: 5_000,
|
|
370
|
+
});
|
|
371
|
+
this.logger.info(`[a2a-adapter] discoverActiveSession: raw result = ${JSON.stringify(raw).slice(0, 500)}`);
|
|
372
|
+
// Unwrap gateway tool standard response: { content: [{ type: "text", text: "..." }] }
|
|
373
|
+
let parsed = raw;
|
|
374
|
+
if (raw?.content?.[0]?.type === "text") {
|
|
375
|
+
try {
|
|
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`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const sessions = parsed?.sessions ?? [];
|
|
384
|
+
this.logger.info(`[a2a-adapter] discoverActiveSession: found ${sessions.length} sessions`);
|
|
385
|
+
const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
|
|
386
|
+
// sessions_list returns "key" not "sessionKey"
|
|
387
|
+
const session = sessions.find((s) => {
|
|
388
|
+
const k = (s.key ?? s.sessionKey);
|
|
389
|
+
return k && !INTERNAL_PREFIXES.some((p) => k.startsWith(p));
|
|
390
|
+
});
|
|
391
|
+
const matchedKey = (session?.key ?? session?.sessionKey);
|
|
392
|
+
if (matchedKey) {
|
|
393
|
+
this.logger.info(`[a2a-adapter] discoverActiveSession: ✓ matched session ${matchedKey}`);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
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)"}`));
|
|
398
|
+
}
|
|
399
|
+
return matchedKey ?? null;
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
this.logger.error(`[a2a-adapter] discoverActiveSession: ✗ caught error — ${err instanceof Error ? err.message : String(err)}, returning null`);
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/** Send a notification to all known targets. Individual failures are silently ignored. */
|
|
407
|
+
async notifyUser(message) {
|
|
408
|
+
const targets = this.getNotificationTargets();
|
|
409
|
+
this.logger.info(`[a2a-adapter] notifyUser: targets=${targets.size}, msgLen=${message.length}, preview="${message.slice(0, 80)}"`);
|
|
410
|
+
if (!this.gatewayConfig) {
|
|
411
|
+
this.logger.warn(`[a2a-adapter] notifyUser: skipped — no gateway config, message lost`);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
// Fallback: no registered targets yet (e.g. right after gateway restart).
|
|
415
|
+
// Discover the active session and send directly via sessions_send.
|
|
416
|
+
if (targets.size === 0) {
|
|
417
|
+
this.logger.info(`[a2a-adapter] notifyUser: no registered targets → falling back to findTargetSession()`);
|
|
418
|
+
const sessionKey = await this.findTargetSession();
|
|
419
|
+
if (sessionKey) {
|
|
420
|
+
this.logger.info(`[a2a-adapter] notifyUser: fallback discovered session ${sessionKey} → calling sessions_send`);
|
|
421
|
+
try {
|
|
422
|
+
await (0, gateway_client_1.invokeGatewayTool)({
|
|
423
|
+
gateway: this.gatewayConfig,
|
|
424
|
+
tool: "sessions_send",
|
|
425
|
+
args: { sessionKey, message },
|
|
426
|
+
timeoutMs: 5_000,
|
|
427
|
+
});
|
|
428
|
+
this.logger.info(`[a2a-adapter] notifyUser: ✓ fallback sessions_send to ${sessionKey} succeeded`);
|
|
429
|
+
// Also register this session for future notifications
|
|
430
|
+
if (this.registerDiscoveredTarget) {
|
|
431
|
+
this.registerDiscoveredTarget(sessionKey);
|
|
432
|
+
this.logger.info(`[a2a-adapter] notifyUser: registered ${sessionKey} as notification target for future use`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
this.logger.error(`[a2a-adapter] notifyUser: ✗ fallback sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
this.logger.warn(`[a2a-adapter] notifyUser: ✗ findTargetSession returned null — no active session found, message lost`);
|
|
441
|
+
}
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
this.logger.info(`[a2a-adapter] notifyUser: sending to ${targets.size} registered target(s): [${[...targets.keys()].join(", ")}]`);
|
|
445
|
+
const results = await Promise.allSettled([...targets.entries()].map(async ([key, target]) => {
|
|
446
|
+
this.logger.info(`[a2a-adapter] notifyUser: → ${key} (type=${target.type})`);
|
|
447
|
+
try {
|
|
448
|
+
await (target.type === "channel"
|
|
449
|
+
? (0, gateway_client_1.invokeGatewayTool)({
|
|
450
|
+
gateway: this.gatewayConfig,
|
|
451
|
+
tool: "message",
|
|
452
|
+
args: { action: "send", target: target.conversationId, message },
|
|
453
|
+
timeoutMs: 5_000,
|
|
454
|
+
})
|
|
455
|
+
: (0, gateway_client_1.invokeGatewayTool)({
|
|
456
|
+
gateway: this.gatewayConfig,
|
|
457
|
+
tool: "sessions_send",
|
|
458
|
+
args: { sessionKey: target.sessionKey, message },
|
|
459
|
+
timeoutMs: 5_000,
|
|
460
|
+
}));
|
|
461
|
+
this.logger.info(`[a2a-adapter] notifyUser: ✓ ${key} (${target.type}) succeeded`);
|
|
462
|
+
}
|
|
463
|
+
catch (err) {
|
|
464
|
+
this.logger.error(`[a2a-adapter] notifyUser: ✗ ${key} (${target.type}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
465
|
+
throw err;
|
|
466
|
+
}
|
|
467
|
+
}));
|
|
468
|
+
const ok = results.filter((r) => r.status === "fulfilled").length;
|
|
469
|
+
const fail = results.filter((r) => r.status === "rejected").length;
|
|
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
|
+
}
|
|
476
|
+
}
|
|
477
|
+
publishMessage(eventBus, text) {
|
|
478
|
+
const message = {
|
|
479
|
+
kind: "message",
|
|
480
|
+
role: "agent",
|
|
481
|
+
messageId: `msg-${Date.now()}`,
|
|
482
|
+
parts: [{ kind: "text", text }],
|
|
483
|
+
};
|
|
484
|
+
eventBus.publish(message);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
exports.OpenClawAgentExecutor = OpenClawAgentExecutor;
|
|
488
|
+
/* ------------------------------------------------------------------ */
|
|
489
|
+
/* Prompt builders */
|
|
490
|
+
/* ------------------------------------------------------------------ */
|
|
491
|
+
/**
|
|
492
|
+
* Build the approval request message injected into the human's active session.
|
|
493
|
+
* The AI in that session will relay it and handle the human's approve/reject response.
|
|
494
|
+
*/
|
|
495
|
+
function buildApprovalRequest(taskId, fromAgentName, taskText) {
|
|
496
|
+
const preview = taskText.length > 600 ? taskText.slice(0, 600) + "…" : taskText;
|
|
497
497
|
return `[MultiClaws] 收到来自 **${fromAgentName}** 的委派任务,需要授权
|
|
498
498
|
|
|
499
499
|
**任务内容:**
|
|
@@ -505,13 +505,13 @@ ${preview}
|
|
|
505
505
|
- 同意:\`multiclaws_task_respond(taskId="${taskId}", approved=true)\`
|
|
506
506
|
- 拒绝:\`multiclaws_task_respond(taskId="${taskId}", approved=false)\`
|
|
507
507
|
|
|
508
|
-
授权等待时间:5 分钟,超时自动拒绝。`;
|
|
509
|
-
}
|
|
510
|
-
/**
|
|
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.
|
|
513
|
-
*/
|
|
514
|
-
function buildA2AMainSessionPrompt(taskId, fromAgentName, taskText) {
|
|
508
|
+
授权等待时间:5 分钟,超时自动拒绝。`;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
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.
|
|
513
|
+
*/
|
|
514
|
+
function buildA2AMainSessionPrompt(taskId, fromAgentName, taskText) {
|
|
515
515
|
return `[MultiClaws 委派任务] 来自 **${fromAgentName}**:
|
|
516
516
|
|
|
517
517
|
${taskText}
|
|
@@ -521,5 +521,5 @@ ${taskText}
|
|
|
521
521
|
- taskId: "${taskId}"
|
|
522
522
|
- result: 你的完整回复内容
|
|
523
523
|
|
|
524
|
-
无论成功还是失败都必须调用,这是结果回传给委派方的唯一方式。`;
|
|
525
|
-
}
|
|
524
|
+
无论成功还是失败都必须调用,这是结果回传给委派方的唯一方式。`;
|
|
525
|
+
}
|