multiclaws 0.4.39 → 0.4.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -16,16 +16,16 @@ export type A2AAdapterOptions = {
|
|
|
16
16
|
};
|
|
17
17
|
};
|
|
18
18
|
/**
|
|
19
|
-
* Bridges the A2A protocol to OpenClaw's
|
|
19
|
+
* Bridges the A2A protocol to OpenClaw's session injection mechanism.
|
|
20
20
|
*
|
|
21
21
|
* When a remote agent sends a task via A2A `message/send`,
|
|
22
22
|
* this executor:
|
|
23
23
|
* 1. Classifies the task risk (safe vs risky)
|
|
24
|
-
* 2.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* 4.
|
|
28
|
-
* 5. Waits for the
|
|
24
|
+
* 2. For risky tasks: pushes approval request to the user's active session and waits
|
|
25
|
+
* For safe tasks: proceeds immediately
|
|
26
|
+
* 3. Finds the target session (where user last sent a message, or main session)
|
|
27
|
+
* 4. Injects the task into that session via sessions_send — no isolated sub-session created
|
|
28
|
+
* 5. Waits for the session AI to call back via `multiclaws_a2a_callback`
|
|
29
29
|
* 6. Returns the final result as a Message
|
|
30
30
|
*/
|
|
31
31
|
export declare class OpenClawAgentExecutor implements AgentExecutor {
|
|
@@ -61,6 +61,13 @@ export declare class OpenClawAgentExecutor implements AgentExecutor {
|
|
|
61
61
|
* or rejects on timeout or cancellation.
|
|
62
62
|
*/
|
|
63
63
|
private createApprovalCallback;
|
|
64
|
+
/**
|
|
65
|
+
* Find the best target session for task injection:
|
|
66
|
+
* 1. Prefer the session where the user most recently sent a message (role === "user")
|
|
67
|
+
* 2. Fall back to the first non-internal active session (typically the main webchat session)
|
|
68
|
+
* Never returns internal sessions (delegate-*, a2a-*).
|
|
69
|
+
*/
|
|
70
|
+
private findTargetSession;
|
|
64
71
|
/**
|
|
65
72
|
* Discover the most recently active non-internal session via sessions_list.
|
|
66
73
|
* Used as fallback when no notification targets have been registered yet
|
|
@@ -58,16 +58,16 @@ function extractTextFromMessage(message) {
|
|
|
58
58
|
.join("\n");
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
* Bridges the A2A protocol to OpenClaw's
|
|
61
|
+
* Bridges the A2A protocol to OpenClaw's session injection mechanism.
|
|
62
62
|
*
|
|
63
63
|
* When a remote agent sends a task via A2A `message/send`,
|
|
64
64
|
* this executor:
|
|
65
65
|
* 1. Classifies the task risk (safe vs risky)
|
|
66
|
-
* 2.
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* 4.
|
|
70
|
-
* 5. Waits for the
|
|
66
|
+
* 2. For risky tasks: pushes approval request to the user's active session and waits
|
|
67
|
+
* For safe tasks: proceeds immediately
|
|
68
|
+
* 3. Finds the target session (where user last sent a message, or main session)
|
|
69
|
+
* 4. Injects the task into that session via sessions_send — no isolated sub-session created
|
|
70
|
+
* 5. Waits for the session AI to call back via `multiclaws_a2a_callback`
|
|
71
71
|
* 6. Returns the final result as a Message
|
|
72
72
|
*/
|
|
73
73
|
class OpenClawAgentExecutor {
|
|
@@ -114,24 +114,25 @@ class OpenClawAgentExecutor {
|
|
|
114
114
|
eventBus.finished();
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
|
-
//
|
|
117
|
+
// ── Step 1: Risk classification ──
|
|
118
118
|
const risk = classifyTaskRisk(taskText);
|
|
119
|
-
this.logger.info(`[a2a-adapter] task ${taskId} risk=${risk}`);
|
|
119
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:risk-classify] risk=${risk}, text="${taskText.slice(0, 60)}"`);
|
|
120
120
|
if (risk === "risky") {
|
|
121
|
-
//
|
|
121
|
+
// ── Step 2a: Approval gate (risky tasks only) ──
|
|
122
122
|
const approvalTimeoutMs = 5 * 60 * 1000; // 5 minutes
|
|
123
123
|
const approvalPromise = this.createApprovalCallback(taskId, approvalTimeoutMs);
|
|
124
|
-
this.logger.info(`[a2a-adapter] task ${taskId}
|
|
124
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-request] sending approval request to user (timeout=${approvalTimeoutMs / 1000}s)`);
|
|
125
125
|
void this.notifyUser(buildApprovalRequest(taskId, fromAgentName, taskText));
|
|
126
126
|
let approved;
|
|
127
127
|
try {
|
|
128
128
|
approved = await approvalPromise;
|
|
129
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-result] user responded: approved=${approved}`);
|
|
129
130
|
}
|
|
130
131
|
catch (err) {
|
|
132
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
131
133
|
const isCanceled = err instanceof Error && err.message === "canceled";
|
|
132
134
|
if (isCanceled) {
|
|
133
|
-
|
|
134
|
-
this.logger.info(`[a2a-adapter] task ${taskId} canceled during approval wait`);
|
|
135
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-result] caught "canceled" error → aborting task`);
|
|
135
136
|
this.taskTracker.update(taskId, { status: "failed", error: "canceled" });
|
|
136
137
|
this.publishMessage(eventBus, "Task was canceled.");
|
|
137
138
|
eventBus.finished();
|
|
@@ -139,61 +140,66 @@ class OpenClawAgentExecutor {
|
|
|
139
140
|
}
|
|
140
141
|
// Approval timed out → auto-reject
|
|
141
142
|
approved = false;
|
|
142
|
-
this.logger.warn(`[a2a-adapter] task ${taskId} approval
|
|
143
|
+
this.logger.warn(`[a2a-adapter] task ${taskId} [step:approval-result] caught error: ${errMsg} → treating as auto-reject`);
|
|
143
144
|
}
|
|
144
145
|
if (!approved) {
|
|
145
146
|
const reason = "用户拒绝或未在超时时间内授权。";
|
|
146
|
-
this.logger.info(`[a2a-adapter] task ${taskId} rejected`);
|
|
147
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-rejected] → aborting task, reason: ${reason}`);
|
|
147
148
|
this.taskTracker.update(taskId, { status: "failed", error: reason });
|
|
148
149
|
this.publishMessage(eventBus, `任务已被拒绝:${reason}`);
|
|
149
150
|
eventBus.finished();
|
|
150
151
|
return;
|
|
151
152
|
}
|
|
152
|
-
this.logger.info(`[a2a-adapter] task ${taskId}
|
|
153
|
-
void this.notifyUser(`✅ 已授权,开始执行来自 **${fromAgentName}** 的任务…`);
|
|
153
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:approval-passed] → proceeding to find target session`);
|
|
154
154
|
}
|
|
155
155
|
else {
|
|
156
|
-
|
|
157
|
-
this.logger.info(`[a2a-adapter] task ${taskId} safe query — auto-executing`);
|
|
158
|
-
void this.notifyUser(`📨 收到来自 **${fromAgentName}** 的查询任务(安全,自动执行):\n\n${taskText.slice(0, 300)}`);
|
|
156
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:auto-execute] safe query, skipping approval → proceeding to find target session`);
|
|
159
157
|
}
|
|
158
|
+
// ── Step 3: Find target session ──
|
|
159
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:find-session] calling findTargetSession()`);
|
|
160
|
+
const targetSessionKey = await this.findTargetSession();
|
|
161
|
+
if (!targetSessionKey) {
|
|
162
|
+
const errMsg = "无法找到用户活跃 session,任务未执行。请确保至少有一个活跃的对话 session。";
|
|
163
|
+
this.logger.error(`[a2a-adapter] task ${taskId} [step:find-session] ✗ no target session found → aborting task`);
|
|
164
|
+
this.taskTracker.update(taskId, { status: "failed", error: errMsg });
|
|
165
|
+
this.publishMessage(eventBus, errMsg);
|
|
166
|
+
eventBus.finished();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:find-session] ✓ target session = ${targetSessionKey}`);
|
|
160
170
|
try {
|
|
161
|
-
//
|
|
171
|
+
// ── Step 4: Register callback ──
|
|
162
172
|
const timeoutMs = 180_000;
|
|
163
173
|
const resultPromise = this.createCallback(taskId, timeoutMs);
|
|
164
|
-
this.logger.info(`[a2a-adapter] task ${taskId} callback registered (timeout=${timeoutMs / 1000}s)`);
|
|
165
|
-
//
|
|
166
|
-
const prompt =
|
|
167
|
-
this.logger.info(`[a2a-adapter] task ${taskId}
|
|
168
|
-
|
|
174
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:register-callback] callback registered (timeout=${timeoutMs / 1000}s, pending total=${this.pendingCallbacks.size})`);
|
|
175
|
+
// ── Step 5: Inject task into target session ──
|
|
176
|
+
const prompt = buildA2AMainSessionPrompt(taskId, fromAgentName, taskText);
|
|
177
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:inject-task] calling sessions_send(sessionKey=${targetSessionKey}, promptLen=${prompt.length})`);
|
|
178
|
+
await (0, gateway_client_1.invokeGatewayTool)({
|
|
169
179
|
gateway: this.gatewayConfig,
|
|
170
|
-
tool: "
|
|
171
|
-
args: {
|
|
172
|
-
task: prompt,
|
|
173
|
-
mode: "run",
|
|
174
|
-
cwd: this.cwd,
|
|
175
|
-
},
|
|
176
|
-
sessionKey: `a2a-${taskId}`,
|
|
180
|
+
tool: "sessions_send",
|
|
181
|
+
args: { sessionKey: targetSessionKey, message: prompt },
|
|
177
182
|
timeoutMs: 15_000,
|
|
178
183
|
});
|
|
179
|
-
this.logger.info(`[a2a-adapter] task ${taskId}
|
|
180
|
-
|
|
181
|
-
// Wait for the sub-agent to call back
|
|
184
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:inject-task] ✓ sessions_send succeeded → waiting for callback...`);
|
|
185
|
+
// ── Step 6: Wait for callback ──
|
|
182
186
|
const output = await resultPromise;
|
|
183
|
-
// Return result
|
|
187
|
+
// ── Step 7: Return result ──
|
|
184
188
|
this.taskTracker.update(taskId, { status: "completed", result: output });
|
|
185
|
-
this.logger.info(`[a2a-adapter]
|
|
186
|
-
void this.notifyUser(`✅ **来自 ${fromAgentName} 的任务已完成**\n\n${output.slice(0, 800)}`);
|
|
189
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:completed] ✓ resultLen=${output.length}, preview="${output.slice(0, 120)}"`);
|
|
187
190
|
this.publishMessage(eventBus, output || "Task completed with no output.");
|
|
188
191
|
}
|
|
189
192
|
catch (err) {
|
|
190
193
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
191
|
-
|
|
194
|
+
const isCanceled = err instanceof Error && err.message === "canceled";
|
|
195
|
+
const isTimeout = errorMsg.includes("timed out");
|
|
196
|
+
const errorType = isCanceled ? "canceled" : isTimeout ? "timeout" : "error";
|
|
197
|
+
this.logger.error(`[a2a-adapter] task ${taskId} [step:catch] ✗ type=${errorType}, reason: ${errorMsg} → marking failed, notifying user`);
|
|
192
198
|
this.taskTracker.update(taskId, { status: "failed", error: errorMsg });
|
|
193
199
|
void this.notifyUser(`❌ 来自 **${fromAgentName}** 的任务执行失败:${errorMsg}`);
|
|
194
200
|
this.publishMessage(eventBus, `Error: ${errorMsg}`);
|
|
195
201
|
}
|
|
196
|
-
this.logger.info(`[a2a-adapter] task ${taskId} eventBus.finished()`);
|
|
202
|
+
this.logger.info(`[a2a-adapter] task ${taskId} [step:finished] eventBus.finished()`);
|
|
197
203
|
eventBus.finished();
|
|
198
204
|
}
|
|
199
205
|
/**
|
|
@@ -281,54 +287,137 @@ class OpenClawAgentExecutor {
|
|
|
281
287
|
this.pendingApprovals.set(taskId, { resolve, reject, timer });
|
|
282
288
|
});
|
|
283
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Find the best target session for task injection:
|
|
292
|
+
* 1. Prefer the session where the user most recently sent a message (role === "user")
|
|
293
|
+
* 2. Fall back to the first non-internal active session (typically the main webchat session)
|
|
294
|
+
* Never returns internal sessions (delegate-*, a2a-*).
|
|
295
|
+
*/
|
|
296
|
+
async findTargetSession() {
|
|
297
|
+
if (!this.gatewayConfig) {
|
|
298
|
+
this.logger.warn(`[a2a-adapter] findTargetSession: skipped — no gateway config`);
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
this.logger.info(`[a2a-adapter] findTargetSession: calling sessions_list (limit=20, activeMinutes=1440)`);
|
|
303
|
+
const raw = await (0, gateway_client_1.invokeGatewayTool)({
|
|
304
|
+
gateway: this.gatewayConfig,
|
|
305
|
+
tool: "sessions_list",
|
|
306
|
+
args: { limit: 20, activeMinutes: 1440, messageLimit: 3 },
|
|
307
|
+
timeoutMs: 5_000,
|
|
308
|
+
});
|
|
309
|
+
this.logger.info(`[a2a-adapter] findTargetSession: raw result = ${JSON.stringify(raw).slice(0, 500)}`);
|
|
310
|
+
// Unwrap gateway tool standard response: { content: [{ type: "text", text: "..." }] }
|
|
311
|
+
let parsed = raw;
|
|
312
|
+
if (raw?.content?.[0]?.type === "text") {
|
|
313
|
+
try {
|
|
314
|
+
parsed = JSON.parse(raw.content[0].text);
|
|
315
|
+
this.logger.info(`[a2a-adapter] findTargetSession: unwrapped gateway response successfully`);
|
|
316
|
+
}
|
|
317
|
+
catch (parseErr) {
|
|
318
|
+
this.logger.warn(`[a2a-adapter] findTargetSession: failed to parse content[0].text as JSON — ${parseErr instanceof Error ? parseErr.message : String(parseErr)}, using raw object`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
|
|
322
|
+
const sessions = parsed?.sessions ?? [];
|
|
323
|
+
this.logger.info(`[a2a-adapter] findTargetSession: ${sessions.length} total sessions from gateway`);
|
|
324
|
+
const filtered = sessions.filter((s) => {
|
|
325
|
+
const k = (s.key ?? s.sessionKey);
|
|
326
|
+
return k && !INTERNAL_PREFIXES.some((p) => k.startsWith(p));
|
|
327
|
+
});
|
|
328
|
+
this.logger.info(`[a2a-adapter] findTargetSession: ${filtered.length} non-internal sessions after filtering`);
|
|
329
|
+
// Prefer sessions that have at least one user-originated message
|
|
330
|
+
const withUserMsg = filtered.filter((s) => Array.isArray(s.messages) && s.messages.some((m) => m.role === "user"));
|
|
331
|
+
// Fall back to any non-internal session (likely the main webchat session)
|
|
332
|
+
const target = withUserMsg[0] ?? filtered[0];
|
|
333
|
+
const targetKey = (target?.key ?? target?.sessionKey);
|
|
334
|
+
if (targetKey) {
|
|
335
|
+
const source = withUserMsg.length > 0 ? "user-message session" : "fallback non-internal session";
|
|
336
|
+
this.logger.info(`[a2a-adapter] findTargetSession: ✓ matched ${targetKey} (${source}, ${withUserMsg.length} with user msgs, ${filtered.length} total)`);
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
this.logger.warn(`[a2a-adapter] findTargetSession: ✗ no target found (${sessions.length} raw, ${filtered.length} after filter, ${withUserMsg.length} with user msgs)`);
|
|
340
|
+
sessions.forEach((s, i) => {
|
|
341
|
+
const k = (s.key ?? s.sessionKey) ?? "(no key)";
|
|
342
|
+
const msgCount = Array.isArray(s.messages) ? s.messages.length : 0;
|
|
343
|
+
this.logger.info(`[a2a-adapter] findTargetSession: session[${i}]: key=${k}, messages=${msgCount}`);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return targetKey ?? null;
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
this.logger.error(`[a2a-adapter] findTargetSession: ✗ caught error — ${err instanceof Error ? err.message : String(err)}, returning null`);
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
284
353
|
/**
|
|
285
354
|
* Discover the most recently active non-internal session via sessions_list.
|
|
286
355
|
* Used as fallback when no notification targets have been registered yet
|
|
287
356
|
* (e.g. right after a gateway restart before the user sends their first message).
|
|
288
357
|
*/
|
|
289
358
|
async discoverActiveSession() {
|
|
290
|
-
if (!this.gatewayConfig)
|
|
359
|
+
if (!this.gatewayConfig) {
|
|
360
|
+
this.logger.warn(`[a2a-adapter] discoverActiveSession: skipped — no gateway config`);
|
|
291
361
|
return null;
|
|
362
|
+
}
|
|
292
363
|
try {
|
|
293
|
-
|
|
364
|
+
this.logger.info(`[a2a-adapter] discoverActiveSession: calling sessions_list (limit=10, activeMinutes=120)`);
|
|
365
|
+
const raw = await (0, gateway_client_1.invokeGatewayTool)({
|
|
294
366
|
gateway: this.gatewayConfig,
|
|
295
367
|
tool: "sessions_list",
|
|
296
368
|
args: { limit: 10, activeMinutes: 120 },
|
|
297
369
|
timeoutMs: 5_000,
|
|
298
370
|
});
|
|
299
|
-
this.logger.info(`[a2a-adapter] discoverActiveSession: raw result = ${JSON.stringify(
|
|
300
|
-
|
|
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 ?? [];
|
|
301
384
|
this.logger.info(`[a2a-adapter] discoverActiveSession: found ${sessions.length} sessions`);
|
|
302
385
|
const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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}`);
|
|
306
394
|
}
|
|
307
395
|
else {
|
|
308
|
-
this.logger.warn(`[a2a-adapter] discoverActiveSession: all ${sessions.length} sessions filtered or empty`);
|
|
309
|
-
sessions.forEach((s) => this.logger.info(`[a2a-adapter] session:
|
|
396
|
+
this.logger.warn(`[a2a-adapter] discoverActiveSession: ✗ all ${sessions.length} sessions filtered or empty`);
|
|
397
|
+
sessions.forEach((s, i) => this.logger.info(`[a2a-adapter] discoverActiveSession: session[${i}]: key=${(s.key ?? s.sessionKey) ?? "(no key)"}`));
|
|
310
398
|
}
|
|
311
|
-
return
|
|
399
|
+
return matchedKey ?? null;
|
|
312
400
|
}
|
|
313
401
|
catch (err) {
|
|
314
|
-
this.logger.
|
|
402
|
+
this.logger.error(`[a2a-adapter] discoverActiveSession: ✗ caught error — ${err instanceof Error ? err.message : String(err)}, returning null`);
|
|
315
403
|
return null;
|
|
316
404
|
}
|
|
317
405
|
}
|
|
318
406
|
/** Send a notification to all known targets. Individual failures are silently ignored. */
|
|
319
407
|
async notifyUser(message) {
|
|
320
408
|
const targets = this.getNotificationTargets();
|
|
409
|
+
this.logger.info(`[a2a-adapter] notifyUser: targets=${targets.size}, msgLen=${message.length}, preview="${message.slice(0, 80)}"`);
|
|
321
410
|
if (!this.gatewayConfig) {
|
|
322
|
-
this.logger.
|
|
411
|
+
this.logger.warn(`[a2a-adapter] notifyUser: skipped — no gateway config, message lost`);
|
|
323
412
|
return;
|
|
324
413
|
}
|
|
325
414
|
// Fallback: no registered targets yet (e.g. right after gateway restart).
|
|
326
415
|
// Discover the active session and send directly via sessions_send.
|
|
327
416
|
if (targets.size === 0) {
|
|
328
|
-
this.logger.info(`[a2a-adapter] notifyUser: no registered targets
|
|
329
|
-
const sessionKey = await this.
|
|
417
|
+
this.logger.info(`[a2a-adapter] notifyUser: no registered targets → falling back to findTargetSession()`);
|
|
418
|
+
const sessionKey = await this.findTargetSession();
|
|
330
419
|
if (sessionKey) {
|
|
331
|
-
this.logger.info(`[a2a-adapter] notifyUser: discovered session ${sessionKey}
|
|
420
|
+
this.logger.info(`[a2a-adapter] notifyUser: fallback discovered session ${sessionKey} → calling sessions_send`);
|
|
332
421
|
try {
|
|
333
422
|
await (0, gateway_client_1.invokeGatewayTool)({
|
|
334
423
|
gateway: this.gatewayConfig,
|
|
@@ -336,22 +425,25 @@ class OpenClawAgentExecutor {
|
|
|
336
425
|
args: { sessionKey, message },
|
|
337
426
|
timeoutMs: 5_000,
|
|
338
427
|
});
|
|
428
|
+
this.logger.info(`[a2a-adapter] notifyUser: ✓ fallback sessions_send to ${sessionKey} succeeded`);
|
|
339
429
|
// Also register this session for future notifications
|
|
340
430
|
if (this.registerDiscoveredTarget) {
|
|
341
431
|
this.registerDiscoveredTarget(sessionKey);
|
|
432
|
+
this.logger.info(`[a2a-adapter] notifyUser: registered ${sessionKey} as notification target for future use`);
|
|
342
433
|
}
|
|
343
434
|
}
|
|
344
435
|
catch (err) {
|
|
345
|
-
this.logger.
|
|
436
|
+
this.logger.error(`[a2a-adapter] notifyUser: ✗ fallback sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
346
437
|
}
|
|
347
438
|
}
|
|
348
439
|
else {
|
|
349
|
-
this.logger.warn(`[a2a-adapter] notifyUser: no active session found, message lost`);
|
|
440
|
+
this.logger.warn(`[a2a-adapter] notifyUser: ✗ findTargetSession returned null — no active session found, message lost`);
|
|
350
441
|
}
|
|
351
442
|
return;
|
|
352
443
|
}
|
|
444
|
+
this.logger.info(`[a2a-adapter] notifyUser: sending to ${targets.size} registered target(s): [${[...targets.keys()].join(", ")}]`);
|
|
353
445
|
const results = await Promise.allSettled([...targets.entries()].map(async ([key, target]) => {
|
|
354
|
-
this.logger.info(`[a2a-adapter] notifyUser:
|
|
446
|
+
this.logger.info(`[a2a-adapter] notifyUser: → ${key} (type=${target.type})`);
|
|
355
447
|
try {
|
|
356
448
|
await (target.type === "channel"
|
|
357
449
|
? (0, gateway_client_1.invokeGatewayTool)({
|
|
@@ -361,23 +453,26 @@ class OpenClawAgentExecutor {
|
|
|
361
453
|
timeoutMs: 5_000,
|
|
362
454
|
})
|
|
363
455
|
: (0, gateway_client_1.invokeGatewayTool)({
|
|
364
|
-
// sessions_send injects a message into the session so the AI
|
|
365
|
-
// can relay it to the human (correct tool; was "chat.send" before)
|
|
366
456
|
gateway: this.gatewayConfig,
|
|
367
457
|
tool: "sessions_send",
|
|
368
458
|
args: { sessionKey: target.sessionKey, message },
|
|
369
459
|
timeoutMs: 5_000,
|
|
370
460
|
}));
|
|
371
|
-
this.logger.info(`[a2a-adapter] notifyUser:
|
|
461
|
+
this.logger.info(`[a2a-adapter] notifyUser: ✓ ${key} (${target.type}) succeeded`);
|
|
372
462
|
}
|
|
373
463
|
catch (err) {
|
|
374
|
-
this.logger.
|
|
464
|
+
this.logger.error(`[a2a-adapter] notifyUser: ✗ ${key} (${target.type}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
375
465
|
throw err;
|
|
376
466
|
}
|
|
377
467
|
}));
|
|
378
468
|
const ok = results.filter((r) => r.status === "fulfilled").length;
|
|
379
469
|
const fail = results.filter((r) => r.status === "rejected").length;
|
|
380
|
-
|
|
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
|
+
}
|
|
381
476
|
}
|
|
382
477
|
publishMessage(eventBus, text) {
|
|
383
478
|
const message = {
|
|
@@ -413,27 +508,18 @@ ${preview}
|
|
|
413
508
|
授权等待时间:5 分钟,超时自动拒绝。`;
|
|
414
509
|
}
|
|
415
510
|
/**
|
|
416
|
-
* Build the prompt
|
|
417
|
-
* The
|
|
511
|
+
* Build the prompt injected into the user's active main session for an incoming A2A task.
|
|
512
|
+
* The AI in that session processes the task naturally and must call multiclaws_a2a_callback.
|
|
418
513
|
*/
|
|
419
|
-
function
|
|
420
|
-
return
|
|
421
|
-
|
|
422
|
-
## 任务内容
|
|
514
|
+
function buildA2AMainSessionPrompt(taskId, fromAgentName, taskText) {
|
|
515
|
+
return `[MultiClaws 委派任务] 来自 **${fromAgentName}**:
|
|
423
516
|
|
|
424
517
|
${taskText}
|
|
425
518
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
\`\`\`
|
|
431
|
-
multiclaws_a2a_callback(taskId="${taskId}", result="你的完整回复内容")
|
|
432
|
-
\`\`\`
|
|
519
|
+
---
|
|
520
|
+
完成后请调用 \`multiclaws_a2a_callback\` 汇报结果:
|
|
521
|
+
- taskId: "${taskId}"
|
|
522
|
+
- result: 你的完整回复内容
|
|
433
523
|
|
|
434
|
-
|
|
435
|
-
- 无论任务成功还是失败,都必须调用 \`multiclaws_a2a_callback\`
|
|
436
|
-
- result 参数填写你的完整回复文本
|
|
437
|
-
- 如果任务失败,在 result 中说明失败原因
|
|
438
|
-
- 这是唯一的结果回传方式,不调用则结果会丢失`;
|
|
524
|
+
无论成功还是失败都必须调用,这是结果回传给委派方的唯一方式。`;
|
|
439
525
|
}
|
|
@@ -143,7 +143,6 @@ export declare class MulticlawsService extends EventEmitter {
|
|
|
143
143
|
addNotificationTarget(key: string, target: NotificationTarget): void;
|
|
144
144
|
/** Consistent name for this agent: AgentCard.name or fallback. */
|
|
145
145
|
private getFormattedName;
|
|
146
|
-
/** Send a notification to all known targets with detailed logging. */
|
|
147
146
|
/** Discover the most recently active non-internal session via sessions_list. */
|
|
148
147
|
private discoverActiveSession;
|
|
149
148
|
notifyUser(message: string): Promise<void>;
|
|
@@ -256,31 +256,41 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
256
256
|
/* ---------------------------------------------------------------- */
|
|
257
257
|
async delegateTask(params) {
|
|
258
258
|
this.log("info", `[delegate] ▶ delegateTask(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
|
|
259
|
-
this.log("info", `[delegate] task preview: ${params.task.slice(0, 120)}`);
|
|
260
|
-
|
|
259
|
+
this.log("info", `[delegate] task preview: "${params.task.slice(0, 120)}"`);
|
|
260
|
+
// Step 1: Check profile
|
|
261
|
+
this.log("info", `[delegate] [step:profile-check] verifying profile completeness`);
|
|
262
|
+
try {
|
|
263
|
+
await this.requireCompleteProfile();
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
this.log("error", `[delegate] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
|
|
267
|
+
return { status: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
268
|
+
}
|
|
269
|
+
// Step 2: Look up agent
|
|
270
|
+
this.log("info", `[delegate] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
|
|
261
271
|
const agentRecord = await this.agentRegistry.get(params.agentUrl);
|
|
262
272
|
if (!agentRecord) {
|
|
263
|
-
this.log("warn", `[delegate] ✗ unknown agent: ${params.agentUrl}`);
|
|
273
|
+
this.log("warn", `[delegate] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
|
|
264
274
|
return { status: "failed", error: `unknown agent: ${params.agentUrl}` };
|
|
265
275
|
}
|
|
266
|
-
this.log("info", `[delegate] agent found: ${agentRecord.name} (${agentRecord.url})`);
|
|
276
|
+
this.log("info", `[delegate] [step:agent-lookup] ✓ found: ${agentRecord.name} (${agentRecord.url})`);
|
|
277
|
+
// Step 3: Track task
|
|
267
278
|
const track = this.taskTracker.create({
|
|
268
279
|
fromPeerId: "local",
|
|
269
280
|
toPeerId: params.agentUrl,
|
|
270
281
|
task: params.task,
|
|
271
282
|
});
|
|
272
283
|
this.taskTracker.update(track.taskId, { status: "running" });
|
|
273
|
-
this.log("info", `[delegate]
|
|
284
|
+
this.log("info", `[delegate] [step:track] taskId=${track.taskId}, status=running`);
|
|
274
285
|
try {
|
|
275
|
-
|
|
286
|
+
// Step 4: Create A2A client
|
|
287
|
+
this.log("info", `[delegate] ${track.taskId} [step:create-client] creating A2A client for ${agentRecord.url}`);
|
|
276
288
|
const client = await this.createA2AClient(agentRecord);
|
|
277
|
-
this.log("info", `[delegate] ${track.taskId}
|
|
278
|
-
// Fire-and-forget execution
|
|
279
|
-
// the gateway call can return quickly and the task can outlive
|
|
280
|
-
// the gateway's HTTP timeout.
|
|
289
|
+
this.log("info", `[delegate] ${track.taskId} [step:create-client] ✓ client created → starting fire-and-forget send`);
|
|
290
|
+
// Step 5: Fire-and-forget execution
|
|
281
291
|
void (async () => {
|
|
282
292
|
try {
|
|
283
|
-
this.log("info", `[delegate] ${track.taskId} sending A2A message
|
|
293
|
+
this.log("info", `[delegate] ${track.taskId} [step:background-send] sending A2A message to ${agentRecord.name}...`);
|
|
284
294
|
const result = await client.sendMessage({
|
|
285
295
|
message: {
|
|
286
296
|
kind: "message",
|
|
@@ -289,24 +299,22 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
289
299
|
messageId: track.taskId,
|
|
290
300
|
},
|
|
291
301
|
});
|
|
292
|
-
this.log("info", `[delegate] ${track.taskId} A2A response received
|
|
302
|
+
this.log("info", `[delegate] ${track.taskId} [step:background-send] ✓ A2A response received → processing result`);
|
|
293
303
|
this.processTaskResult(track.taskId, result);
|
|
294
304
|
}
|
|
295
305
|
catch (err) {
|
|
296
306
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
297
307
|
this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
|
|
298
|
-
this.log("error", `[delegate]
|
|
308
|
+
this.log("error", `[delegate] ${track.taskId} [step:background-send] ✗ caught error: ${errorMsg} → task marked failed`);
|
|
299
309
|
}
|
|
300
310
|
})();
|
|
301
|
-
|
|
302
|
-
// do not depend on the remote agent's total execution time.
|
|
303
|
-
this.log("info", `[delegate] ${track.taskId} returned immediately (fire-and-forget)`);
|
|
311
|
+
this.log("info", `[delegate] ${track.taskId} [step:return] returned immediately (fire-and-forget), background send in progress`);
|
|
304
312
|
return { taskId: track.taskId, status: "running" };
|
|
305
313
|
}
|
|
306
314
|
catch (err) {
|
|
307
315
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
308
316
|
this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
|
|
309
|
-
this.log("error", `[delegate]
|
|
317
|
+
this.log("error", `[delegate] ${track.taskId} [step:catch] ✗ caught error during client creation: ${errorMsg} → task marked failed`);
|
|
310
318
|
return { taskId: track.taskId, status: "failed", error: errorMsg };
|
|
311
319
|
}
|
|
312
320
|
}
|
|
@@ -316,25 +324,38 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
316
324
|
*/
|
|
317
325
|
async delegateTaskSync(params) {
|
|
318
326
|
this.log("info", `[delegate-sync] ▶ delegateTaskSync(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
|
|
319
|
-
this.log("info", `[delegate-sync] task preview: ${params.task.slice(0, 120)}`);
|
|
320
|
-
|
|
327
|
+
this.log("info", `[delegate-sync] task preview: "${params.task.slice(0, 120)}"`);
|
|
328
|
+
// Step 1: Check profile
|
|
329
|
+
this.log("info", `[delegate-sync] [step:profile-check] verifying profile completeness`);
|
|
330
|
+
try {
|
|
331
|
+
await this.requireCompleteProfile();
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
this.log("error", `[delegate-sync] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
|
|
335
|
+
return { status: "failed", error: err instanceof Error ? err.message : String(err) };
|
|
336
|
+
}
|
|
337
|
+
// Step 2: Look up agent
|
|
338
|
+
this.log("info", `[delegate-sync] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
|
|
321
339
|
const agentRecord = await this.agentRegistry.get(params.agentUrl);
|
|
322
340
|
if (!agentRecord) {
|
|
323
|
-
this.log("warn", `[delegate-sync] ✗ unknown agent: ${params.agentUrl}`);
|
|
341
|
+
this.log("warn", `[delegate-sync] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
|
|
324
342
|
return { status: "failed", error: `unknown agent: ${params.agentUrl}` };
|
|
325
343
|
}
|
|
326
|
-
this.log("info", `[delegate-sync] agent found: ${agentRecord.name} (${agentRecord.url})`);
|
|
344
|
+
this.log("info", `[delegate-sync] [step:agent-lookup] ✓ found: ${agentRecord.name} (${agentRecord.url})`);
|
|
345
|
+
// Step 3: Track task
|
|
327
346
|
const track = this.taskTracker.create({
|
|
328
347
|
fromPeerId: "local",
|
|
329
348
|
toPeerId: params.agentUrl,
|
|
330
349
|
task: params.task,
|
|
331
350
|
});
|
|
332
351
|
this.taskTracker.update(track.taskId, { status: "running" });
|
|
333
|
-
this.log("info", `[delegate-sync]
|
|
352
|
+
this.log("info", `[delegate-sync] [step:track] taskId=${track.taskId}, status=running`);
|
|
334
353
|
try {
|
|
335
|
-
|
|
354
|
+
// Step 4: Create A2A client
|
|
355
|
+
this.log("info", `[delegate-sync] ${track.taskId} [step:create-client] creating A2A client for ${agentRecord.url}`);
|
|
336
356
|
const client = await this.createA2AClient(agentRecord);
|
|
337
|
-
|
|
357
|
+
// Step 5: Send A2A message (synchronous — blocks until response)
|
|
358
|
+
this.log("info", `[delegate-sync] ${track.taskId} [step:send] sending A2A message (sync, metadata: selfUrl=${this.selfUrl}, selfName=${this.agentCard?.name ?? "unknown"})...`);
|
|
338
359
|
const result = await client.sendMessage({
|
|
339
360
|
message: {
|
|
340
361
|
kind: "message",
|
|
@@ -347,15 +368,16 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
347
368
|
},
|
|
348
369
|
},
|
|
349
370
|
});
|
|
350
|
-
this.log("info", `[delegate-sync] ${track.taskId} A2A response received`);
|
|
371
|
+
this.log("info", `[delegate-sync] ${track.taskId} [step:send] ✓ A2A response received → processing result`);
|
|
372
|
+
// Step 6: Process result
|
|
351
373
|
const taskResult = this.processTaskResult(track.taskId, result);
|
|
352
|
-
this.log("info", `[delegate-sync]
|
|
374
|
+
this.log("info", `[delegate-sync] ${track.taskId} [step:completed] ✓ status=${taskResult.status}, outputLen=${taskResult.output?.length ?? 0}, preview="${(taskResult.output ?? "").slice(0, 120)}"`);
|
|
353
375
|
return taskResult;
|
|
354
376
|
}
|
|
355
377
|
catch (err) {
|
|
356
378
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
357
379
|
this.taskTracker.update(track.taskId, { status: "failed", error: errorMsg });
|
|
358
|
-
this.log("error", `[delegate-sync]
|
|
380
|
+
this.log("error", `[delegate-sync] ${track.taskId} [step:catch] ✗ caught error: ${errorMsg} → task marked failed`);
|
|
359
381
|
return { taskId: track.taskId, status: "failed", error: errorMsg };
|
|
360
382
|
}
|
|
361
383
|
}
|
|
@@ -366,30 +388,48 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
366
388
|
*/
|
|
367
389
|
async spawnDelegation(params) {
|
|
368
390
|
this.log("info", `[spawn-delegate] ▶ spawnDelegation(agentUrl=${params.agentUrl}, taskLen=${params.task.length})`);
|
|
369
|
-
this.log("info", `[spawn-delegate] task preview: ${params.task.slice(0, 120)}`);
|
|
370
|
-
|
|
391
|
+
this.log("info", `[spawn-delegate] task preview: "${params.task.slice(0, 120)}"`);
|
|
392
|
+
// Step 1: Check profile
|
|
393
|
+
this.log("info", `[spawn-delegate] [step:profile-check] verifying profile completeness`);
|
|
394
|
+
try {
|
|
395
|
+
await this.requireCompleteProfile();
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
this.log("error", `[spawn-delegate] [step:profile-check] ✗ profile incomplete: ${err instanceof Error ? err.message : String(err)}`);
|
|
399
|
+
throw err;
|
|
400
|
+
}
|
|
401
|
+
// Step 2: Look up agent
|
|
402
|
+
this.log("info", `[spawn-delegate] [step:agent-lookup] looking up agent: ${params.agentUrl}`);
|
|
371
403
|
const agent = await this.agentRegistry.get(params.agentUrl);
|
|
372
404
|
if (!agent) {
|
|
373
|
-
this.log("warn", `[spawn-delegate] ✗ unknown agent: ${params.agentUrl}`);
|
|
405
|
+
this.log("warn", `[spawn-delegate] [step:agent-lookup] ✗ unknown agent: ${params.agentUrl} → aborting`);
|
|
374
406
|
throw new Error(`unknown agent: ${params.agentUrl}`);
|
|
375
407
|
}
|
|
376
|
-
this.log("info", `[spawn-delegate] agent found: ${agent.name} (${agent.url})`);
|
|
408
|
+
this.log("info", `[spawn-delegate] [step:agent-lookup] ✓ found: ${agent.name} (${agent.url})`);
|
|
409
|
+
// Step 3: Check gateway config
|
|
377
410
|
if (!this.gatewayConfig) {
|
|
378
|
-
this.log("error", `[spawn-delegate] ✗ gateway config not available`);
|
|
411
|
+
this.log("error", `[spawn-delegate] [step:gateway-check] ✗ gateway config not available → aborting`);
|
|
379
412
|
throw new Error("gateway config not available — cannot spawn sub-agent");
|
|
380
413
|
}
|
|
414
|
+
// Step 4: Spawn sub-agent
|
|
381
415
|
const prompt = buildDelegationPrompt(agent, params.task);
|
|
382
416
|
const sessionKey = `delegate-${Date.now()}`;
|
|
383
|
-
this.log("info", `[spawn-delegate]
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
417
|
+
this.log("info", `[spawn-delegate] [step:spawn] calling sessions_spawn (cwd=${this.resolvedCwd}, sessionKey=${sessionKey}, promptLen=${prompt.length})`);
|
|
418
|
+
try {
|
|
419
|
+
const spawnResult = await (0, gateway_client_1.invokeGatewayTool)({
|
|
420
|
+
gateway: this.gatewayConfig,
|
|
421
|
+
tool: "sessions_spawn",
|
|
422
|
+
args: { task: prompt, mode: "run", cwd: this.resolvedCwd },
|
|
423
|
+
sessionKey,
|
|
424
|
+
timeoutMs: 15_000,
|
|
425
|
+
});
|
|
426
|
+
this.log("info", `[spawn-delegate] [step:spawn] ✓ sub-agent spawned for ${agent.name} — result=${JSON.stringify(spawnResult).slice(0, 200)}`);
|
|
427
|
+
return { message: `已启动子 agent 向 ${agent.name} 委派任务` };
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
this.log("error", `[spawn-delegate] [step:spawn] ✗ sessions_spawn failed: ${err instanceof Error ? err.message : String(err)} → aborting`);
|
|
431
|
+
throw err;
|
|
432
|
+
}
|
|
393
433
|
}
|
|
394
434
|
getTaskStatus(taskId) {
|
|
395
435
|
return this.taskTracker.get(taskId);
|
|
@@ -871,7 +911,16 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
871
911
|
}
|
|
872
912
|
}
|
|
873
913
|
async createA2AClient(agent) {
|
|
874
|
-
|
|
914
|
+
this.log("info", `[a2a-client] creating client for ${agent.name} (${agent.url})`);
|
|
915
|
+
try {
|
|
916
|
+
const client = await this.clientFactory.createFromUrl(agent.url);
|
|
917
|
+
this.log("info", `[a2a-client] ✓ client created for ${agent.name} (${agent.url})`);
|
|
918
|
+
return client;
|
|
919
|
+
}
|
|
920
|
+
catch (err) {
|
|
921
|
+
this.log("error", `[a2a-client] ✗ failed to create client for ${agent.name} (${agent.url}): ${err instanceof Error ? err.message : String(err)}`);
|
|
922
|
+
throw err;
|
|
923
|
+
}
|
|
875
924
|
}
|
|
876
925
|
/**
|
|
877
926
|
* Send a message using A2A streaming to minimize latency.
|
|
@@ -979,49 +1028,67 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
979
1028
|
getFormattedName() {
|
|
980
1029
|
return this.agentCard?.name ?? "OpenClaw Agent";
|
|
981
1030
|
}
|
|
982
|
-
/** Send a notification to all known targets with detailed logging. */
|
|
983
1031
|
/** Discover the most recently active non-internal session via sessions_list. */
|
|
984
1032
|
async discoverActiveSession() {
|
|
985
|
-
if (!this.gatewayConfig)
|
|
1033
|
+
if (!this.gatewayConfig) {
|
|
1034
|
+
this.log("warn", `discoverActiveSession: skipped — no gateway config`);
|
|
986
1035
|
return null;
|
|
1036
|
+
}
|
|
987
1037
|
try {
|
|
988
|
-
|
|
1038
|
+
this.log("info", `discoverActiveSession: calling sessions_list (limit=10, activeMinutes=120)`);
|
|
1039
|
+
const raw = await (0, gateway_client_1.invokeGatewayTool)({
|
|
989
1040
|
gateway: this.gatewayConfig,
|
|
990
1041
|
tool: "sessions_list",
|
|
991
1042
|
args: { limit: 10, activeMinutes: 120 },
|
|
992
1043
|
timeoutMs: 5_000,
|
|
993
1044
|
});
|
|
994
|
-
this.log("info", `discoverActiveSession: raw result = ${JSON.stringify(
|
|
995
|
-
|
|
996
|
-
|
|
1045
|
+
this.log("info", `discoverActiveSession: raw result = ${JSON.stringify(raw).slice(0, 500)}`);
|
|
1046
|
+
// Unwrap gateway tool standard response: { content: [{ type: "text", text: "..." }] }
|
|
1047
|
+
let parsed = raw;
|
|
1048
|
+
if (raw?.content?.[0]?.type === "text") {
|
|
1049
|
+
try {
|
|
1050
|
+
parsed = JSON.parse(raw.content[0].text);
|
|
1051
|
+
this.log("info", `discoverActiveSession: unwrapped gateway response successfully`);
|
|
1052
|
+
}
|
|
1053
|
+
catch (parseErr) {
|
|
1054
|
+
this.log("warn", `discoverActiveSession: failed to parse content[0].text as JSON — ${parseErr instanceof Error ? parseErr.message : String(parseErr)}, using raw object`);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const sessions = parsed?.sessions ?? [];
|
|
1058
|
+
this.log("info", `discoverActiveSession: found ${sessions.length} sessions from gateway`);
|
|
997
1059
|
const INTERNAL_PREFIXES = ["delegate-", "a2a-"];
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1060
|
+
// sessions_list returns "key" not "sessionKey"
|
|
1061
|
+
const session = sessions.find((s) => {
|
|
1062
|
+
const k = (s.key ?? s.sessionKey);
|
|
1063
|
+
return k && !INTERNAL_PREFIXES.some((p) => k.startsWith(p));
|
|
1064
|
+
});
|
|
1065
|
+
const matchedKey = (session?.key ?? session?.sessionKey);
|
|
1066
|
+
if (matchedKey) {
|
|
1067
|
+
this.log("info", `discoverActiveSession: ✓ matched session ${matchedKey}`);
|
|
1001
1068
|
}
|
|
1002
1069
|
else {
|
|
1003
|
-
this.log("warn", `discoverActiveSession: all ${sessions.length} sessions filtered or empty`);
|
|
1004
|
-
sessions.forEach((s) => this.log("info", `
|
|
1070
|
+
this.log("warn", `discoverActiveSession: ✗ all ${sessions.length} sessions filtered or empty`);
|
|
1071
|
+
sessions.forEach((s, i) => this.log("info", `discoverActiveSession: session[${i}]: key=${(s.key ?? s.sessionKey) ?? "(no key)"}`));
|
|
1005
1072
|
}
|
|
1006
|
-
return
|
|
1073
|
+
return matchedKey ?? null;
|
|
1007
1074
|
}
|
|
1008
1075
|
catch (err) {
|
|
1009
|
-
this.log("
|
|
1076
|
+
this.log("error", `discoverActiveSession: ✗ caught error — ${err instanceof Error ? err.message : String(err)}, returning null`);
|
|
1010
1077
|
return null;
|
|
1011
1078
|
}
|
|
1012
1079
|
}
|
|
1013
1080
|
async notifyUser(message) {
|
|
1014
|
-
this.log("info", `notifyUser: targets=${this.notificationTargets.size},
|
|
1081
|
+
this.log("info", `notifyUser: targets=${this.notificationTargets.size}, msgLen=${message.length}, preview="${message.slice(0, 80)}"`);
|
|
1015
1082
|
if (!this.gatewayConfig) {
|
|
1016
|
-
this.log("warn", "notifyUser: skipped — no gatewayConfig");
|
|
1083
|
+
this.log("warn", "notifyUser: skipped — no gatewayConfig, message lost");
|
|
1017
1084
|
return;
|
|
1018
1085
|
}
|
|
1019
1086
|
// Fallback: no registered targets yet (e.g. right after gateway restart)
|
|
1020
1087
|
if (this.notificationTargets.size === 0) {
|
|
1021
|
-
this.log("
|
|
1088
|
+
this.log("info", "notifyUser: no registered targets → falling back to discoverActiveSession()");
|
|
1022
1089
|
const sessionKey = await this.discoverActiveSession();
|
|
1023
1090
|
if (sessionKey) {
|
|
1024
|
-
this.log("info", `notifyUser: discovered session ${sessionKey}`);
|
|
1091
|
+
this.log("info", `notifyUser: fallback discovered session ${sessionKey} → calling sessions_send`);
|
|
1025
1092
|
try {
|
|
1026
1093
|
await (0, gateway_client_1.invokeGatewayTool)({
|
|
1027
1094
|
gateway: this.gatewayConfig,
|
|
@@ -1029,20 +1096,23 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
1029
1096
|
args: { sessionKey, message },
|
|
1030
1097
|
timeoutMs: 5_000,
|
|
1031
1098
|
});
|
|
1099
|
+
this.log("info", `notifyUser: ✓ fallback sessions_send to ${sessionKey} succeeded`);
|
|
1032
1100
|
this.addNotificationTarget(`web:${sessionKey}`, { type: "web", sessionKey });
|
|
1101
|
+
this.log("info", `notifyUser: registered ${sessionKey} as notification target for future use`);
|
|
1033
1102
|
}
|
|
1034
1103
|
catch (err) {
|
|
1035
|
-
this.log("
|
|
1104
|
+
this.log("error", `notifyUser: ✗ fallback sessions_send to ${sessionKey} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1036
1105
|
}
|
|
1037
1106
|
}
|
|
1038
1107
|
else {
|
|
1039
|
-
this.log("warn", "notifyUser: no active session found, message lost");
|
|
1108
|
+
this.log("warn", "notifyUser: ✗ discoverActiveSession returned null — no active session found, message lost");
|
|
1040
1109
|
}
|
|
1041
1110
|
return;
|
|
1042
1111
|
}
|
|
1043
1112
|
const entries = [...this.notificationTargets.entries()];
|
|
1113
|
+
this.log("info", `notifyUser: sending to ${entries.length} registered target(s): [${entries.map(([k]) => k).join(", ")}]`);
|
|
1044
1114
|
const results = await Promise.allSettled(entries.map(async ([key, target]) => {
|
|
1045
|
-
this.log("info", `notifyUser:
|
|
1115
|
+
this.log("info", `notifyUser: → ${key} (type=${target.type})`);
|
|
1046
1116
|
try {
|
|
1047
1117
|
await (target.type === "channel"
|
|
1048
1118
|
? (0, gateway_client_1.invokeGatewayTool)({
|
|
@@ -1052,26 +1122,28 @@ class MulticlawsService extends node_events_1.EventEmitter {
|
|
|
1052
1122
|
timeoutMs: 5_000,
|
|
1053
1123
|
})
|
|
1054
1124
|
: (0, gateway_client_1.invokeGatewayTool)({
|
|
1055
|
-
// sessions_send injects a message into the session so the AI
|
|
1056
|
-
// can relay it to the human (correct tool; was "chat.send" before)
|
|
1057
1125
|
gateway: this.gatewayConfig,
|
|
1058
1126
|
tool: "sessions_send",
|
|
1059
1127
|
args: { sessionKey: target.sessionKey, message },
|
|
1060
1128
|
timeoutMs: 5_000,
|
|
1061
1129
|
}));
|
|
1062
|
-
this.log("info", `notifyUser: ${key} (${target.type}) succeeded`);
|
|
1130
|
+
this.log("info", `notifyUser: ✓ ${key} (${target.type}) succeeded`);
|
|
1063
1131
|
}
|
|
1064
1132
|
catch (err) {
|
|
1065
|
-
this.log("
|
|
1133
|
+
this.log("error", `notifyUser: ✗ ${key} (${target.type}) failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1066
1134
|
throw err;
|
|
1067
1135
|
}
|
|
1068
1136
|
}));
|
|
1137
|
+
const okCount = results.filter((r) => r.status === "fulfilled").length;
|
|
1069
1138
|
const failCount = results.filter((r) => r.status === "rejected").length;
|
|
1070
|
-
if (failCount ===
|
|
1071
|
-
this.log("
|
|
1139
|
+
if (failCount === 0) {
|
|
1140
|
+
this.log("info", `notifyUser: ✓ all ${okCount} targets succeeded`);
|
|
1141
|
+
}
|
|
1142
|
+
else if (failCount === entries.length) {
|
|
1143
|
+
this.log("error", `notifyUser: ✗ ALL ${failCount} targets failed`);
|
|
1072
1144
|
}
|
|
1073
|
-
else
|
|
1074
|
-
this.log("warn", `notifyUser: ${failCount}
|
|
1145
|
+
else {
|
|
1146
|
+
this.log("warn", `notifyUser: ${okCount} ok, ${failCount} FAILED out of ${entries.length} targets`);
|
|
1075
1147
|
}
|
|
1076
1148
|
}
|
|
1077
1149
|
log(level, message) {
|