@alfe.ai/openclaw-chat 0.0.19 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugin.d.cts +7 -0
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin2.cjs +196 -23
- package/dist/plugin2.js +196 -23
- package/package.json +2 -2
package/dist/plugin.d.cts
CHANGED
|
@@ -225,6 +225,13 @@ interface PluginApi {
|
|
|
225
225
|
config?: Record<string, unknown>;
|
|
226
226
|
runtime?: PluginRuntime;
|
|
227
227
|
registerChannel(channel: ReturnType<typeof createAlfeChannelPlugin>): void;
|
|
228
|
+
registerTool?(tool: {
|
|
229
|
+
name: string;
|
|
230
|
+
description: string;
|
|
231
|
+
label: string;
|
|
232
|
+
parameters: Record<string, unknown>;
|
|
233
|
+
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
234
|
+
}): void;
|
|
228
235
|
registerGatewayMethod?(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;
|
|
229
236
|
on(event: string, handler: (...args: unknown[]) => void | Promise<void>, options?: {
|
|
230
237
|
priority?: number;
|
package/dist/plugin.d.ts
CHANGED
|
@@ -225,6 +225,13 @@ interface PluginApi {
|
|
|
225
225
|
config?: Record<string, unknown>;
|
|
226
226
|
runtime?: PluginRuntime;
|
|
227
227
|
registerChannel(channel: ReturnType<typeof createAlfeChannelPlugin>): void;
|
|
228
|
+
registerTool?(tool: {
|
|
229
|
+
name: string;
|
|
230
|
+
description: string;
|
|
231
|
+
label: string;
|
|
232
|
+
parameters: Record<string, unknown>;
|
|
233
|
+
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
234
|
+
}): void;
|
|
228
235
|
registerGatewayMethod?(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;
|
|
229
236
|
on(event: string, handler: (...args: unknown[]) => void | Promise<void>, options?: {
|
|
230
237
|
priority?: number;
|
package/dist/plugin2.cjs
CHANGED
|
@@ -151,7 +151,7 @@ function isAlfeSessionKey(key) {
|
|
|
151
151
|
}
|
|
152
152
|
/**
|
|
153
153
|
* Extract the channel mode from a standardized session key or conversationId.
|
|
154
|
-
* Returns the mode segment (e.g. 'sms', 'whatsapp', 'chat') or fallback.
|
|
154
|
+
* Returns the mode segment (e.g. 'sms', 'whatsapp', 'chat', 'a2a') or fallback.
|
|
155
155
|
*/
|
|
156
156
|
function extractChannelMode(conversationId, fallback = "chat") {
|
|
157
157
|
return /^alfe:(\w+):/.exec(conversationId)?.[1] ?? fallback;
|
|
@@ -181,7 +181,6 @@ function sessionPath(sessionId) {
|
|
|
181
181
|
}
|
|
182
182
|
async function cleanupOldSessions() {
|
|
183
183
|
if (Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return;
|
|
184
|
-
lastCleanupAt = Date.now();
|
|
185
184
|
try {
|
|
186
185
|
const jsonFiles = (await (0, node_fs_promises.readdir)(SESSIONS_DIR)).filter((f) => f.endsWith(".json"));
|
|
187
186
|
if (jsonFiles.length <= MAX_SESSIONS) {
|
|
@@ -211,6 +210,7 @@ async function cleanupOldSessions() {
|
|
|
211
210
|
remaining--;
|
|
212
211
|
} catch {}
|
|
213
212
|
}
|
|
213
|
+
lastCleanupAt = Date.now();
|
|
214
214
|
} catch {}
|
|
215
215
|
}
|
|
216
216
|
async function getSession(sessionId) {
|
|
@@ -242,17 +242,24 @@ async function createSession(sessionId, agentId, channel, tenantId, userId) {
|
|
|
242
242
|
cleanupOldSessions();
|
|
243
243
|
return session;
|
|
244
244
|
}
|
|
245
|
+
const writeLocks = /* @__PURE__ */ new Map();
|
|
245
246
|
async function addMessage(sessionId, role, content, senderId, senderName) {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
247
|
+
const next = (writeLocks.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
248
|
+
const session = await getSession(sessionId);
|
|
249
|
+
if (!session) return;
|
|
250
|
+
session.messages.push({
|
|
251
|
+
role,
|
|
252
|
+
content,
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
...senderId ? { senderId } : {},
|
|
255
|
+
...senderName ? { senderName } : {}
|
|
256
|
+
});
|
|
257
|
+
await saveSession(session);
|
|
258
|
+
}).finally(() => {
|
|
259
|
+
if (writeLocks.get(sessionId) === next) writeLocks.delete(sessionId);
|
|
254
260
|
});
|
|
255
|
-
|
|
261
|
+
writeLocks.set(sessionId, next);
|
|
262
|
+
await next;
|
|
256
263
|
}
|
|
257
264
|
async function listSessions(filters, limit = 50) {
|
|
258
265
|
await ensureDir();
|
|
@@ -289,6 +296,112 @@ async function listSessions(filters, limit = 50) {
|
|
|
289
296
|
return summaries.slice(0, limit);
|
|
290
297
|
}
|
|
291
298
|
//#endregion
|
|
299
|
+
//#region src/a2a-tools.ts
|
|
300
|
+
function ok(result) {
|
|
301
|
+
return { content: [{
|
|
302
|
+
type: "text",
|
|
303
|
+
text: JSON.stringify(result)
|
|
304
|
+
}] };
|
|
305
|
+
}
|
|
306
|
+
function errResult(message) {
|
|
307
|
+
return { content: [{
|
|
308
|
+
type: "text",
|
|
309
|
+
text: JSON.stringify({ error: message })
|
|
310
|
+
}] };
|
|
311
|
+
}
|
|
312
|
+
function defineTool(def) {
|
|
313
|
+
return {
|
|
314
|
+
name: def.name,
|
|
315
|
+
description: def.description,
|
|
316
|
+
label: def.name,
|
|
317
|
+
parameters: def.parameters,
|
|
318
|
+
execute: async (_toolCallId, params) => {
|
|
319
|
+
try {
|
|
320
|
+
return ok(await def.handler(params));
|
|
321
|
+
} catch (e) {
|
|
322
|
+
return errResult(e.message);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function buildA2ATools(chatClient, log) {
|
|
328
|
+
return [
|
|
329
|
+
defineTool({
|
|
330
|
+
name: "list_agents",
|
|
331
|
+
description: "List other agents in your organization that you can communicate with.",
|
|
332
|
+
parameters: {
|
|
333
|
+
type: "object",
|
|
334
|
+
properties: {},
|
|
335
|
+
required: []
|
|
336
|
+
},
|
|
337
|
+
handler: async () => {
|
|
338
|
+
log.info("list_agents tool called");
|
|
339
|
+
return await chatClient.sendRequest("a2a.list-agents", {});
|
|
340
|
+
}
|
|
341
|
+
}),
|
|
342
|
+
defineTool({
|
|
343
|
+
name: "message_agent",
|
|
344
|
+
description: "Send a message to another agent. Starts a new conversation thread, or continues an existing one. The conversation will bounce back and forth automatically until one agent calls end_conversation() or says [RESOLVED].",
|
|
345
|
+
parameters: {
|
|
346
|
+
type: "object",
|
|
347
|
+
properties: {
|
|
348
|
+
agent_id: {
|
|
349
|
+
type: "string",
|
|
350
|
+
description: "Target agent ID (use list_agents to find available agents)"
|
|
351
|
+
},
|
|
352
|
+
message: {
|
|
353
|
+
type: "string",
|
|
354
|
+
description: "Message to send to the agent"
|
|
355
|
+
},
|
|
356
|
+
thread_id: {
|
|
357
|
+
type: "string",
|
|
358
|
+
description: "Continue an existing conversation thread (omit to start a new one)"
|
|
359
|
+
},
|
|
360
|
+
max_depth: {
|
|
361
|
+
type: "number",
|
|
362
|
+
description: "Maximum number of back-and-forth exchanges (default: 10)"
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
required: ["agent_id", "message"]
|
|
366
|
+
},
|
|
367
|
+
handler: async (params) => {
|
|
368
|
+
const agentId = params.agent_id;
|
|
369
|
+
const message = params.message;
|
|
370
|
+
const threadId = params.thread_id;
|
|
371
|
+
const maxDepth = params.max_depth ?? 10;
|
|
372
|
+
if (!agentId || !message) throw new Error("agent_id and message are required");
|
|
373
|
+
log.info(`message_agent tool: sending to ${agentId}`);
|
|
374
|
+
return await chatClient.sendRequest("a2a.send", {
|
|
375
|
+
targetAgentId: agentId,
|
|
376
|
+
message,
|
|
377
|
+
threadId,
|
|
378
|
+
maxDepth
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}),
|
|
382
|
+
defineTool({
|
|
383
|
+
name: "end_conversation",
|
|
384
|
+
description: "End the current agent-to-agent conversation. Call this when the discussion is resolved and no further exchanges are needed.",
|
|
385
|
+
parameters: {
|
|
386
|
+
type: "object",
|
|
387
|
+
properties: { summary: {
|
|
388
|
+
type: "string",
|
|
389
|
+
description: "Brief summary of what was discussed or resolved"
|
|
390
|
+
} },
|
|
391
|
+
required: []
|
|
392
|
+
},
|
|
393
|
+
handler: (params) => {
|
|
394
|
+
const summary = params.summary;
|
|
395
|
+
log.info(`end_conversation tool called${summary ? `: ${summary}` : ""}`);
|
|
396
|
+
return Promise.resolve({
|
|
397
|
+
status: "conversation_ended",
|
|
398
|
+
summary
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
];
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
292
405
|
//#region src/plugin.ts
|
|
293
406
|
/**
|
|
294
407
|
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
@@ -325,20 +438,30 @@ let pluginRuntime = null;
|
|
|
325
438
|
let chatClient = null;
|
|
326
439
|
let connectingPromise = null;
|
|
327
440
|
let metricsClient = null;
|
|
441
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
442
|
+
const DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
328
443
|
async function downloadAttachments(attachments, log) {
|
|
329
444
|
const attachDir = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "attachments");
|
|
330
445
|
await (0, node_fs_promises.mkdir)(attachDir, { recursive: true });
|
|
331
446
|
const results = [];
|
|
332
447
|
for (const att of attachments) {
|
|
333
|
-
const filename = att.filename ?? att.id;
|
|
448
|
+
const filename = (att.filename ?? att.id).replace(/[/\\]/g, "_").replace(/\.\./g, "_").replace(/\0/g, "");
|
|
334
449
|
const localPath = (0, node_path.join)(attachDir, `${att.id}_${filename}`);
|
|
450
|
+
const controller = new AbortController();
|
|
451
|
+
const timeout = setTimeout(() => {
|
|
452
|
+
controller.abort();
|
|
453
|
+
}, DOWNLOAD_TIMEOUT_MS);
|
|
335
454
|
try {
|
|
336
|
-
const res = await fetch(att.url);
|
|
455
|
+
const res = await fetch(att.url, { signal: controller.signal });
|
|
337
456
|
if (!res.ok) {
|
|
338
457
|
log.warn(`Failed to download attachment ${att.id}: ${String(res.status)}`);
|
|
339
458
|
continue;
|
|
340
459
|
}
|
|
341
460
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
461
|
+
if (buffer.length > MAX_FILE_SIZE) {
|
|
462
|
+
log.warn(`Attachment ${att.id} exceeds max size (${String(buffer.length)} bytes) — skipping`);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
342
465
|
await (0, node_fs_promises.writeFile)(localPath, buffer);
|
|
343
466
|
results.push({
|
|
344
467
|
localPath,
|
|
@@ -348,6 +471,8 @@ async function downloadAttachments(attachments, log) {
|
|
|
348
471
|
log.info(`Downloaded attachment: ${localPath} (${String(buffer.length)} bytes)`);
|
|
349
472
|
} catch (err) {
|
|
350
473
|
log.error(`Failed to download attachment ${att.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
474
|
+
} finally {
|
|
475
|
+
clearTimeout(timeout);
|
|
351
476
|
}
|
|
352
477
|
}
|
|
353
478
|
return results;
|
|
@@ -362,7 +487,8 @@ async function handleAgentRequest(request, log) {
|
|
|
362
487
|
chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
|
|
363
488
|
return;
|
|
364
489
|
}
|
|
365
|
-
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName, attachments: rawAttachments } = request.params;
|
|
490
|
+
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName, attachments: rawAttachments, a2a } = request.params;
|
|
491
|
+
const isA2A = !!a2a;
|
|
366
492
|
if (!message && !rawAttachments?.length) {
|
|
367
493
|
chatClient?.sendResponse(request.id, false, { message: "Missing message" });
|
|
368
494
|
return;
|
|
@@ -389,11 +515,13 @@ async function handleAgentRequest(request, log) {
|
|
|
389
515
|
try {
|
|
390
516
|
const downloadedFiles = rawAttachments?.length ? await downloadAttachments(rawAttachments, log) : [];
|
|
391
517
|
const bodyForAgent = downloadedFiles.length ? `${message || ""}\n\n[Attached files:\n${downloadedFiles.map((f) => `- ${f.filename}: ${f.localPath}`).join("\n")}]` : void 0;
|
|
392
|
-
const channelMode = extractChannelMode(conversationId ?? "", clientType ?? "chat");
|
|
393
|
-
const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : "Alfe";
|
|
518
|
+
const channelMode = isA2A ? "a2a" : extractChannelMode(conversationId ?? "", clientType ?? "chat");
|
|
519
|
+
const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : channelMode === "a2a" ? "Agent" : "Alfe";
|
|
394
520
|
const shortConvId = conversationId?.slice(-8) ?? "";
|
|
395
521
|
const userLabel = displayName ?? userId ?? senderId;
|
|
396
522
|
const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
|
|
523
|
+
let a2aResponseBuffer = "";
|
|
524
|
+
let a2aResolved = false;
|
|
397
525
|
resolvedOpenClawKey = (await dispatchInbound({
|
|
398
526
|
cfg,
|
|
399
527
|
runtime: { channel: runtime.channel },
|
|
@@ -420,12 +548,29 @@ async function handleAgentRequest(request, log) {
|
|
|
420
548
|
...clientType ? { ClientType: clientType } : {},
|
|
421
549
|
...conversationId ? { ConversationId: conversationId } : {},
|
|
422
550
|
...displayName ? { SenderName: displayName } : {},
|
|
423
|
-
ChannelMode: channelMode
|
|
551
|
+
ChannelMode: channelMode,
|
|
552
|
+
...isA2A ? {
|
|
553
|
+
CallerType: "agent",
|
|
554
|
+
CallerAgentId: a2a.sourceAgentId,
|
|
555
|
+
CallerAgentName: a2a.sourceAgentName,
|
|
556
|
+
InteractionDepth: String(a2a.depth),
|
|
557
|
+
A2ASystemPrompt: [
|
|
558
|
+
`This is an agent-to-agent conversation with ${a2a.sourceAgentName}.`,
|
|
559
|
+
"Rules:",
|
|
560
|
+
"- Only respond if you have new information, a question, or an action to coordinate.",
|
|
561
|
+
"- If the conversation is complete, call the end_conversation() tool OR end your message with [RESOLVED].",
|
|
562
|
+
"- Do NOT respond just to acknowledge — that creates infinite loops."
|
|
563
|
+
].join("\n")
|
|
564
|
+
} : {}
|
|
424
565
|
},
|
|
425
566
|
deliver: async (payload) => {
|
|
426
567
|
const responseText = payload.text ?? "";
|
|
427
568
|
const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
|
|
428
569
|
await addMessage(sessionId, "assistant", responseText);
|
|
570
|
+
if (isA2A) {
|
|
571
|
+
a2aResponseBuffer += responseText;
|
|
572
|
+
if (responseText.includes("\"status\":\"conversation_ended\"") || responseText.includes("\"status\": \"conversation_ended\"")) a2aResolved = true;
|
|
573
|
+
}
|
|
429
574
|
chatClient?.notify("agent-message", {
|
|
430
575
|
conversationId: conversationId ?? legacySessionKey,
|
|
431
576
|
text: responseText,
|
|
@@ -437,7 +582,7 @@ async function handleAgentRequest(request, log) {
|
|
|
437
582
|
channel: "alfe",
|
|
438
583
|
role: "assistant"
|
|
439
584
|
}).catch((err) => {
|
|
440
|
-
log.
|
|
585
|
+
log.warn(`Metrics report failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
441
586
|
});
|
|
442
587
|
},
|
|
443
588
|
onRecordError: (err) => {
|
|
@@ -447,6 +592,12 @@ async function handleAgentRequest(request, log) {
|
|
|
447
592
|
log.error(`Dispatch error (${info.kind}): ${err instanceof Error ? err.message : String(err)}`);
|
|
448
593
|
}
|
|
449
594
|
})).route.sessionKey;
|
|
595
|
+
if (isA2A && a2aResponseBuffer) chatClient?.notify("a2a-complete", {
|
|
596
|
+
conversationId: conversationId ?? legacySessionKey,
|
|
597
|
+
fullText: a2aResponseBuffer,
|
|
598
|
+
a2a,
|
|
599
|
+
resolved: a2aResolved
|
|
600
|
+
});
|
|
450
601
|
chatClient?.sendResponse(request.id, true, { sessionKey: resolvedOpenClawKey });
|
|
451
602
|
log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
|
|
452
603
|
} catch (err) {
|
|
@@ -516,10 +667,15 @@ const plugin = {
|
|
|
516
667
|
activate(api) {
|
|
517
668
|
const log = api.logger;
|
|
518
669
|
const alreadyActivated = globalThis.__alfeChatPluginActivated === true;
|
|
519
|
-
|
|
670
|
+
const isGatewayMode = !!api.runtime;
|
|
520
671
|
const alfeChannel = createAlfeChannelPlugin();
|
|
521
672
|
api.registerChannel(alfeChannel);
|
|
522
673
|
log.info(`Registered channel: ${alfeChannel.id}`);
|
|
674
|
+
if (!isGatewayMode) {
|
|
675
|
+
log.debug("Management command context — skipping persistent resource init");
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
globalThis.__alfeChatPluginActivated = true;
|
|
523
679
|
if (!alreadyActivated) {
|
|
524
680
|
log.info("Chat plugin registering...");
|
|
525
681
|
resolveOpenClawSdk(log);
|
|
@@ -547,10 +703,17 @@ const plugin = {
|
|
|
547
703
|
wsUrl: chatWsUrl,
|
|
548
704
|
apiKey,
|
|
549
705
|
onRequest: (request) => {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
706
|
+
const handle = async () => {
|
|
707
|
+
if (request.method === "agent") await handleAgentRequest(request, log);
|
|
708
|
+
else if (request.method === "sessions.list") await handleSessionsList(request, log);
|
|
709
|
+
else if (request.method === "sessions.get") await handleSessionsGet(request, log);
|
|
710
|
+
else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
|
|
711
|
+
};
|
|
712
|
+
handle().catch((err) => {
|
|
713
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
714
|
+
log.error(`Request handler crashed: ${errMsg}`);
|
|
715
|
+
chatClient?.sendResponse(request.id, false, { message: "Internal error" });
|
|
716
|
+
});
|
|
554
717
|
},
|
|
555
718
|
onConnectionChange: (connected) => {
|
|
556
719
|
log.info(`Chat service connection: ${connected ? "connected" : "disconnected"}`);
|
|
@@ -599,20 +762,30 @@ const plugin = {
|
|
|
599
762
|
});
|
|
600
763
|
log.info("Registered gateway RPC methods: sessions.list, sessions.get");
|
|
601
764
|
}
|
|
765
|
+
if (!alreadyActivated && typeof api.registerTool === "function") connectingPromise?.then(() => {
|
|
766
|
+
if (chatClient && typeof api.registerTool === "function") {
|
|
767
|
+
const a2aTools = buildA2ATools(chatClient, log);
|
|
768
|
+
for (const tool of a2aTools) api.registerTool(tool);
|
|
769
|
+
log.info(`Registered ${String(a2aTools.length)} agent-to-agent tools`);
|
|
770
|
+
}
|
|
771
|
+
}).catch(() => {});
|
|
602
772
|
if (!alreadyActivated) {
|
|
603
773
|
api.on("session_start", async (...eventArgs) => {
|
|
774
|
+
if (!pluginRuntime) return;
|
|
604
775
|
const key = eventArgs[0].sessionKey;
|
|
605
776
|
if (!key || isAlfeSessionKey(key)) return;
|
|
606
777
|
log.info(`Chat session starting: ${key}`);
|
|
607
778
|
await createSession(key, "", "alfe");
|
|
608
779
|
}, { priority: 50 });
|
|
609
780
|
api.on("message_received", async (...eventArgs) => {
|
|
781
|
+
if (!pluginRuntime) return;
|
|
610
782
|
const event = eventArgs[0];
|
|
611
783
|
const ctx = eventArgs[1];
|
|
612
784
|
if (!ctx.conversationId || ctx.channelId === "alfe") return;
|
|
613
785
|
await addMessage(ctx.conversationId, "user", event.content, event.from);
|
|
614
786
|
});
|
|
615
787
|
api.on("session_end", (...eventArgs) => {
|
|
788
|
+
if (!pluginRuntime) return;
|
|
616
789
|
const key = eventArgs[0].sessionKey;
|
|
617
790
|
if (!key || !isAlfeSessionKey(key)) return;
|
|
618
791
|
log.info(`Chat session ending: ${key}`);
|
package/dist/plugin2.js
CHANGED
|
@@ -151,7 +151,7 @@ function isAlfeSessionKey(key) {
|
|
|
151
151
|
}
|
|
152
152
|
/**
|
|
153
153
|
* Extract the channel mode from a standardized session key or conversationId.
|
|
154
|
-
* Returns the mode segment (e.g. 'sms', 'whatsapp', 'chat') or fallback.
|
|
154
|
+
* Returns the mode segment (e.g. 'sms', 'whatsapp', 'chat', 'a2a') or fallback.
|
|
155
155
|
*/
|
|
156
156
|
function extractChannelMode(conversationId, fallback = "chat") {
|
|
157
157
|
return /^alfe:(\w+):/.exec(conversationId)?.[1] ?? fallback;
|
|
@@ -181,7 +181,6 @@ function sessionPath(sessionId) {
|
|
|
181
181
|
}
|
|
182
182
|
async function cleanupOldSessions() {
|
|
183
183
|
if (Date.now() - lastCleanupAt < CLEANUP_INTERVAL_MS) return;
|
|
184
|
-
lastCleanupAt = Date.now();
|
|
185
184
|
try {
|
|
186
185
|
const jsonFiles = (await readdir(SESSIONS_DIR)).filter((f) => f.endsWith(".json"));
|
|
187
186
|
if (jsonFiles.length <= MAX_SESSIONS) {
|
|
@@ -211,6 +210,7 @@ async function cleanupOldSessions() {
|
|
|
211
210
|
remaining--;
|
|
212
211
|
} catch {}
|
|
213
212
|
}
|
|
213
|
+
lastCleanupAt = Date.now();
|
|
214
214
|
} catch {}
|
|
215
215
|
}
|
|
216
216
|
async function getSession(sessionId) {
|
|
@@ -242,17 +242,24 @@ async function createSession(sessionId, agentId, channel, tenantId, userId) {
|
|
|
242
242
|
cleanupOldSessions();
|
|
243
243
|
return session;
|
|
244
244
|
}
|
|
245
|
+
const writeLocks = /* @__PURE__ */ new Map();
|
|
245
246
|
async function addMessage(sessionId, role, content, senderId, senderName) {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
247
|
+
const next = (writeLocks.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
248
|
+
const session = await getSession(sessionId);
|
|
249
|
+
if (!session) return;
|
|
250
|
+
session.messages.push({
|
|
251
|
+
role,
|
|
252
|
+
content,
|
|
253
|
+
timestamp: Date.now(),
|
|
254
|
+
...senderId ? { senderId } : {},
|
|
255
|
+
...senderName ? { senderName } : {}
|
|
256
|
+
});
|
|
257
|
+
await saveSession(session);
|
|
258
|
+
}).finally(() => {
|
|
259
|
+
if (writeLocks.get(sessionId) === next) writeLocks.delete(sessionId);
|
|
254
260
|
});
|
|
255
|
-
|
|
261
|
+
writeLocks.set(sessionId, next);
|
|
262
|
+
await next;
|
|
256
263
|
}
|
|
257
264
|
async function listSessions(filters, limit = 50) {
|
|
258
265
|
await ensureDir();
|
|
@@ -289,6 +296,112 @@ async function listSessions(filters, limit = 50) {
|
|
|
289
296
|
return summaries.slice(0, limit);
|
|
290
297
|
}
|
|
291
298
|
//#endregion
|
|
299
|
+
//#region src/a2a-tools.ts
|
|
300
|
+
function ok(result) {
|
|
301
|
+
return { content: [{
|
|
302
|
+
type: "text",
|
|
303
|
+
text: JSON.stringify(result)
|
|
304
|
+
}] };
|
|
305
|
+
}
|
|
306
|
+
function errResult(message) {
|
|
307
|
+
return { content: [{
|
|
308
|
+
type: "text",
|
|
309
|
+
text: JSON.stringify({ error: message })
|
|
310
|
+
}] };
|
|
311
|
+
}
|
|
312
|
+
function defineTool(def) {
|
|
313
|
+
return {
|
|
314
|
+
name: def.name,
|
|
315
|
+
description: def.description,
|
|
316
|
+
label: def.name,
|
|
317
|
+
parameters: def.parameters,
|
|
318
|
+
execute: async (_toolCallId, params) => {
|
|
319
|
+
try {
|
|
320
|
+
return ok(await def.handler(params));
|
|
321
|
+
} catch (e) {
|
|
322
|
+
return errResult(e.message);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function buildA2ATools(chatClient, log) {
|
|
328
|
+
return [
|
|
329
|
+
defineTool({
|
|
330
|
+
name: "list_agents",
|
|
331
|
+
description: "List other agents in your organization that you can communicate with.",
|
|
332
|
+
parameters: {
|
|
333
|
+
type: "object",
|
|
334
|
+
properties: {},
|
|
335
|
+
required: []
|
|
336
|
+
},
|
|
337
|
+
handler: async () => {
|
|
338
|
+
log.info("list_agents tool called");
|
|
339
|
+
return await chatClient.sendRequest("a2a.list-agents", {});
|
|
340
|
+
}
|
|
341
|
+
}),
|
|
342
|
+
defineTool({
|
|
343
|
+
name: "message_agent",
|
|
344
|
+
description: "Send a message to another agent. Starts a new conversation thread, or continues an existing one. The conversation will bounce back and forth automatically until one agent calls end_conversation() or says [RESOLVED].",
|
|
345
|
+
parameters: {
|
|
346
|
+
type: "object",
|
|
347
|
+
properties: {
|
|
348
|
+
agent_id: {
|
|
349
|
+
type: "string",
|
|
350
|
+
description: "Target agent ID (use list_agents to find available agents)"
|
|
351
|
+
},
|
|
352
|
+
message: {
|
|
353
|
+
type: "string",
|
|
354
|
+
description: "Message to send to the agent"
|
|
355
|
+
},
|
|
356
|
+
thread_id: {
|
|
357
|
+
type: "string",
|
|
358
|
+
description: "Continue an existing conversation thread (omit to start a new one)"
|
|
359
|
+
},
|
|
360
|
+
max_depth: {
|
|
361
|
+
type: "number",
|
|
362
|
+
description: "Maximum number of back-and-forth exchanges (default: 10)"
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
required: ["agent_id", "message"]
|
|
366
|
+
},
|
|
367
|
+
handler: async (params) => {
|
|
368
|
+
const agentId = params.agent_id;
|
|
369
|
+
const message = params.message;
|
|
370
|
+
const threadId = params.thread_id;
|
|
371
|
+
const maxDepth = params.max_depth ?? 10;
|
|
372
|
+
if (!agentId || !message) throw new Error("agent_id and message are required");
|
|
373
|
+
log.info(`message_agent tool: sending to ${agentId}`);
|
|
374
|
+
return await chatClient.sendRequest("a2a.send", {
|
|
375
|
+
targetAgentId: agentId,
|
|
376
|
+
message,
|
|
377
|
+
threadId,
|
|
378
|
+
maxDepth
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}),
|
|
382
|
+
defineTool({
|
|
383
|
+
name: "end_conversation",
|
|
384
|
+
description: "End the current agent-to-agent conversation. Call this when the discussion is resolved and no further exchanges are needed.",
|
|
385
|
+
parameters: {
|
|
386
|
+
type: "object",
|
|
387
|
+
properties: { summary: {
|
|
388
|
+
type: "string",
|
|
389
|
+
description: "Brief summary of what was discussed or resolved"
|
|
390
|
+
} },
|
|
391
|
+
required: []
|
|
392
|
+
},
|
|
393
|
+
handler: (params) => {
|
|
394
|
+
const summary = params.summary;
|
|
395
|
+
log.info(`end_conversation tool called${summary ? `: ${summary}` : ""}`);
|
|
396
|
+
return Promise.resolve({
|
|
397
|
+
status: "conversation_ended",
|
|
398
|
+
summary
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
];
|
|
403
|
+
}
|
|
404
|
+
//#endregion
|
|
292
405
|
//#region src/plugin.ts
|
|
293
406
|
/**
|
|
294
407
|
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
@@ -325,20 +438,30 @@ let pluginRuntime = null;
|
|
|
325
438
|
let chatClient = null;
|
|
326
439
|
let connectingPromise = null;
|
|
327
440
|
let metricsClient = null;
|
|
441
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
442
|
+
const DOWNLOAD_TIMEOUT_MS = 3e4;
|
|
328
443
|
async function downloadAttachments(attachments, log) {
|
|
329
444
|
const attachDir = join(homedir(), ".alfe", "attachments");
|
|
330
445
|
await mkdir(attachDir, { recursive: true });
|
|
331
446
|
const results = [];
|
|
332
447
|
for (const att of attachments) {
|
|
333
|
-
const filename = att.filename ?? att.id;
|
|
448
|
+
const filename = (att.filename ?? att.id).replace(/[/\\]/g, "_").replace(/\.\./g, "_").replace(/\0/g, "");
|
|
334
449
|
const localPath = join(attachDir, `${att.id}_${filename}`);
|
|
450
|
+
const controller = new AbortController();
|
|
451
|
+
const timeout = setTimeout(() => {
|
|
452
|
+
controller.abort();
|
|
453
|
+
}, DOWNLOAD_TIMEOUT_MS);
|
|
335
454
|
try {
|
|
336
|
-
const res = await fetch(att.url);
|
|
455
|
+
const res = await fetch(att.url, { signal: controller.signal });
|
|
337
456
|
if (!res.ok) {
|
|
338
457
|
log.warn(`Failed to download attachment ${att.id}: ${String(res.status)}`);
|
|
339
458
|
continue;
|
|
340
459
|
}
|
|
341
460
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
461
|
+
if (buffer.length > MAX_FILE_SIZE) {
|
|
462
|
+
log.warn(`Attachment ${att.id} exceeds max size (${String(buffer.length)} bytes) — skipping`);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
342
465
|
await writeFile(localPath, buffer);
|
|
343
466
|
results.push({
|
|
344
467
|
localPath,
|
|
@@ -348,6 +471,8 @@ async function downloadAttachments(attachments, log) {
|
|
|
348
471
|
log.info(`Downloaded attachment: ${localPath} (${String(buffer.length)} bytes)`);
|
|
349
472
|
} catch (err) {
|
|
350
473
|
log.error(`Failed to download attachment ${att.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
474
|
+
} finally {
|
|
475
|
+
clearTimeout(timeout);
|
|
351
476
|
}
|
|
352
477
|
}
|
|
353
478
|
return results;
|
|
@@ -362,7 +487,8 @@ async function handleAgentRequest(request, log) {
|
|
|
362
487
|
chatClient?.sendResponse(request.id, false, { message: "OpenClaw SDK not available — cannot dispatch" });
|
|
363
488
|
return;
|
|
364
489
|
}
|
|
365
|
-
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName, attachments: rawAttachments } = request.params;
|
|
490
|
+
const { message, sessionKey: legacySessionKey, userId, conversationId, conversationType, tenantId, clientType, displayName, attachments: rawAttachments, a2a } = request.params;
|
|
491
|
+
const isA2A = !!a2a;
|
|
366
492
|
if (!message && !rawAttachments?.length) {
|
|
367
493
|
chatClient?.sendResponse(request.id, false, { message: "Missing message" });
|
|
368
494
|
return;
|
|
@@ -389,11 +515,13 @@ async function handleAgentRequest(request, log) {
|
|
|
389
515
|
try {
|
|
390
516
|
const downloadedFiles = rawAttachments?.length ? await downloadAttachments(rawAttachments, log) : [];
|
|
391
517
|
const bodyForAgent = downloadedFiles.length ? `${message || ""}\n\n[Attached files:\n${downloadedFiles.map((f) => `- ${f.filename}: ${f.localPath}`).join("\n")}]` : void 0;
|
|
392
|
-
const channelMode = extractChannelMode(conversationId ?? "", clientType ?? "chat");
|
|
393
|
-
const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : "Alfe";
|
|
518
|
+
const channelMode = isA2A ? "a2a" : extractChannelMode(conversationId ?? "", clientType ?? "chat");
|
|
519
|
+
const channelLabel = channelMode === "sms" ? "SMS" : channelMode === "whatsapp" ? "WhatsApp" : channelMode === "a2a" ? "Agent" : "Alfe";
|
|
394
520
|
const shortConvId = conversationId?.slice(-8) ?? "";
|
|
395
521
|
const userLabel = displayName ?? userId ?? senderId;
|
|
396
522
|
const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
|
|
523
|
+
let a2aResponseBuffer = "";
|
|
524
|
+
let a2aResolved = false;
|
|
397
525
|
resolvedOpenClawKey = (await dispatchInbound({
|
|
398
526
|
cfg,
|
|
399
527
|
runtime: { channel: runtime.channel },
|
|
@@ -420,12 +548,29 @@ async function handleAgentRequest(request, log) {
|
|
|
420
548
|
...clientType ? { ClientType: clientType } : {},
|
|
421
549
|
...conversationId ? { ConversationId: conversationId } : {},
|
|
422
550
|
...displayName ? { SenderName: displayName } : {},
|
|
423
|
-
ChannelMode: channelMode
|
|
551
|
+
ChannelMode: channelMode,
|
|
552
|
+
...isA2A ? {
|
|
553
|
+
CallerType: "agent",
|
|
554
|
+
CallerAgentId: a2a.sourceAgentId,
|
|
555
|
+
CallerAgentName: a2a.sourceAgentName,
|
|
556
|
+
InteractionDepth: String(a2a.depth),
|
|
557
|
+
A2ASystemPrompt: [
|
|
558
|
+
`This is an agent-to-agent conversation with ${a2a.sourceAgentName}.`,
|
|
559
|
+
"Rules:",
|
|
560
|
+
"- Only respond if you have new information, a question, or an action to coordinate.",
|
|
561
|
+
"- If the conversation is complete, call the end_conversation() tool OR end your message with [RESOLVED].",
|
|
562
|
+
"- Do NOT respond just to acknowledge — that creates infinite loops."
|
|
563
|
+
].join("\n")
|
|
564
|
+
} : {}
|
|
424
565
|
},
|
|
425
566
|
deliver: async (payload) => {
|
|
426
567
|
const responseText = payload.text ?? "";
|
|
427
568
|
const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
|
|
428
569
|
await addMessage(sessionId, "assistant", responseText);
|
|
570
|
+
if (isA2A) {
|
|
571
|
+
a2aResponseBuffer += responseText;
|
|
572
|
+
if (responseText.includes("\"status\":\"conversation_ended\"") || responseText.includes("\"status\": \"conversation_ended\"")) a2aResolved = true;
|
|
573
|
+
}
|
|
429
574
|
chatClient?.notify("agent-message", {
|
|
430
575
|
conversationId: conversationId ?? legacySessionKey,
|
|
431
576
|
text: responseText,
|
|
@@ -437,7 +582,7 @@ async function handleAgentRequest(request, log) {
|
|
|
437
582
|
channel: "alfe",
|
|
438
583
|
role: "assistant"
|
|
439
584
|
}).catch((err) => {
|
|
440
|
-
log.
|
|
585
|
+
log.warn(`Metrics report failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
441
586
|
});
|
|
442
587
|
},
|
|
443
588
|
onRecordError: (err) => {
|
|
@@ -447,6 +592,12 @@ async function handleAgentRequest(request, log) {
|
|
|
447
592
|
log.error(`Dispatch error (${info.kind}): ${err instanceof Error ? err.message : String(err)}`);
|
|
448
593
|
}
|
|
449
594
|
})).route.sessionKey;
|
|
595
|
+
if (isA2A && a2aResponseBuffer) chatClient?.notify("a2a-complete", {
|
|
596
|
+
conversationId: conversationId ?? legacySessionKey,
|
|
597
|
+
fullText: a2aResponseBuffer,
|
|
598
|
+
a2a,
|
|
599
|
+
resolved: a2aResolved
|
|
600
|
+
});
|
|
450
601
|
chatClient?.sendResponse(request.id, true, { sessionKey: resolvedOpenClawKey });
|
|
451
602
|
log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
|
|
452
603
|
} catch (err) {
|
|
@@ -516,10 +667,15 @@ const plugin = {
|
|
|
516
667
|
activate(api) {
|
|
517
668
|
const log = api.logger;
|
|
518
669
|
const alreadyActivated = globalThis.__alfeChatPluginActivated === true;
|
|
519
|
-
|
|
670
|
+
const isGatewayMode = !!api.runtime;
|
|
520
671
|
const alfeChannel = createAlfeChannelPlugin();
|
|
521
672
|
api.registerChannel(alfeChannel);
|
|
522
673
|
log.info(`Registered channel: ${alfeChannel.id}`);
|
|
674
|
+
if (!isGatewayMode) {
|
|
675
|
+
log.debug("Management command context — skipping persistent resource init");
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
globalThis.__alfeChatPluginActivated = true;
|
|
523
679
|
if (!alreadyActivated) {
|
|
524
680
|
log.info("Chat plugin registering...");
|
|
525
681
|
resolveOpenClawSdk(log);
|
|
@@ -547,10 +703,17 @@ const plugin = {
|
|
|
547
703
|
wsUrl: chatWsUrl,
|
|
548
704
|
apiKey,
|
|
549
705
|
onRequest: (request) => {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
706
|
+
const handle = async () => {
|
|
707
|
+
if (request.method === "agent") await handleAgentRequest(request, log);
|
|
708
|
+
else if (request.method === "sessions.list") await handleSessionsList(request, log);
|
|
709
|
+
else if (request.method === "sessions.get") await handleSessionsGet(request, log);
|
|
710
|
+
else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
|
|
711
|
+
};
|
|
712
|
+
handle().catch((err) => {
|
|
713
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
714
|
+
log.error(`Request handler crashed: ${errMsg}`);
|
|
715
|
+
chatClient?.sendResponse(request.id, false, { message: "Internal error" });
|
|
716
|
+
});
|
|
554
717
|
},
|
|
555
718
|
onConnectionChange: (connected) => {
|
|
556
719
|
log.info(`Chat service connection: ${connected ? "connected" : "disconnected"}`);
|
|
@@ -599,20 +762,30 @@ const plugin = {
|
|
|
599
762
|
});
|
|
600
763
|
log.info("Registered gateway RPC methods: sessions.list, sessions.get");
|
|
601
764
|
}
|
|
765
|
+
if (!alreadyActivated && typeof api.registerTool === "function") connectingPromise?.then(() => {
|
|
766
|
+
if (chatClient && typeof api.registerTool === "function") {
|
|
767
|
+
const a2aTools = buildA2ATools(chatClient, log);
|
|
768
|
+
for (const tool of a2aTools) api.registerTool(tool);
|
|
769
|
+
log.info(`Registered ${String(a2aTools.length)} agent-to-agent tools`);
|
|
770
|
+
}
|
|
771
|
+
}).catch(() => {});
|
|
602
772
|
if (!alreadyActivated) {
|
|
603
773
|
api.on("session_start", async (...eventArgs) => {
|
|
774
|
+
if (!pluginRuntime) return;
|
|
604
775
|
const key = eventArgs[0].sessionKey;
|
|
605
776
|
if (!key || isAlfeSessionKey(key)) return;
|
|
606
777
|
log.info(`Chat session starting: ${key}`);
|
|
607
778
|
await createSession(key, "", "alfe");
|
|
608
779
|
}, { priority: 50 });
|
|
609
780
|
api.on("message_received", async (...eventArgs) => {
|
|
781
|
+
if (!pluginRuntime) return;
|
|
610
782
|
const event = eventArgs[0];
|
|
611
783
|
const ctx = eventArgs[1];
|
|
612
784
|
if (!ctx.conversationId || ctx.channelId === "alfe") return;
|
|
613
785
|
await addMessage(ctx.conversationId, "user", event.content, event.from);
|
|
614
786
|
});
|
|
615
787
|
api.on("session_end", (...eventArgs) => {
|
|
788
|
+
if (!pluginRuntime) return;
|
|
616
789
|
const key = eventArgs[0].sessionKey;
|
|
617
790
|
if (!key || !isAlfeSessionKey(key)) return;
|
|
618
791
|
log.info(`Chat session ending: ${key}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/openclaw-chat",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/plugin.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@alfe.ai/agent-api-client": "^0.0.7",
|
|
31
|
-
"@alfe.ai/chat": "^0.0.
|
|
31
|
+
"@alfe.ai/chat": "^0.0.7",
|
|
32
32
|
"@alfe.ai/config": "^0.0.7"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|