@alfe.ai/openclaw-chat 0.1.6 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +7 -1
- package/dist/plugin.d.cts +23 -1
- package/dist/plugin.d.ts +23 -1
- package/dist/plugin.js +2 -2
- package/dist/plugin2.cjs +194 -23
- package/dist/plugin2.d.cts +2 -2
- package/dist/plugin2.d.ts +2 -2
- package/dist/plugin2.js +183 -24
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as AlfeChannelConfig, i as createAlfeChannelPlugin, n as plugin, o as AlfePluginConfig, s as AlfeResolvedAccount } from "./plugin.cjs";
|
|
2
2
|
|
|
3
3
|
//#region src/session-store.d.ts
|
|
4
4
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as AlfeChannelConfig, i as createAlfeChannelPlugin, n as plugin, o as AlfePluginConfig, s as AlfeResolvedAccount } from "./plugin.js";
|
|
2
2
|
|
|
3
3
|
//#region src/session-store.d.ts
|
|
4
4
|
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as createAlfeChannelPlugin, n as plugin } from "./plugin2.js";
|
|
2
2
|
export { createAlfeChannelPlugin, plugin as default };
|
package/dist/plugin.cjs
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
|
+
Object.defineProperties(exports, {
|
|
2
|
+
__esModule: { value: true },
|
|
3
|
+
[Symbol.toStringTag]: { value: "Module" }
|
|
4
|
+
});
|
|
1
5
|
const require_plugin = require("./plugin2.cjs");
|
|
2
|
-
|
|
6
|
+
exports.buildA2ACompletePayload = require_plugin.buildA2ACompletePayload;
|
|
7
|
+
exports.default = require_plugin.plugin;
|
|
8
|
+
exports.stripLeakedToolSummary = require_plugin.stripLeakedToolSummary;
|
package/dist/plugin.d.cts
CHANGED
|
@@ -274,6 +274,28 @@ interface PluginServiceContext {
|
|
|
274
274
|
stateDir: string;
|
|
275
275
|
logger: PluginLogger;
|
|
276
276
|
}
|
|
277
|
+
declare function stripLeakedToolSummary(text: string): string;
|
|
278
|
+
/**
|
|
279
|
+
* Build the `a2a-complete` notify payload. ALWAYS fired for an A2A turn —
|
|
280
|
+
* even when the dispatch produced no relayable text — so the chat service can
|
|
281
|
+
* emit an explicit terminal/stop frame instead of the thread dead-ending
|
|
282
|
+
* silently. `terminated`/`reason` are set when there's nothing to relay
|
|
283
|
+
* (`empty_turn`) or the agent ended the thread via end_conversation
|
|
284
|
+
* (`resolved`).
|
|
285
|
+
*/
|
|
286
|
+
declare function buildA2ACompletePayload<A>(params: {
|
|
287
|
+
conversationId: string;
|
|
288
|
+
fullText: string;
|
|
289
|
+
a2a: A;
|
|
290
|
+
resolved: boolean;
|
|
291
|
+
}): {
|
|
292
|
+
conversationId: string;
|
|
293
|
+
fullText: string;
|
|
294
|
+
a2a: A;
|
|
295
|
+
resolved: boolean;
|
|
296
|
+
terminated?: boolean;
|
|
297
|
+
reason?: string;
|
|
298
|
+
};
|
|
277
299
|
interface PluginApi {
|
|
278
300
|
logger: PluginLogger;
|
|
279
301
|
registrationMode?: 'full' | 'setup-only' | 'setup-runtime' | 'cli-metadata';
|
|
@@ -306,4 +328,4 @@ declare const plugin: {
|
|
|
306
328
|
deactivate(api: PluginApi): Promise<void>;
|
|
307
329
|
};
|
|
308
330
|
//#endregion
|
|
309
|
-
export {
|
|
331
|
+
export { AlfeChannelConfig as a, createAlfeChannelPlugin as i, plugin as n, AlfePluginConfig as o, stripLeakedToolSummary as r, AlfeResolvedAccount as s, buildA2ACompletePayload as t };
|
package/dist/plugin.d.ts
CHANGED
|
@@ -274,6 +274,28 @@ interface PluginServiceContext {
|
|
|
274
274
|
stateDir: string;
|
|
275
275
|
logger: PluginLogger;
|
|
276
276
|
}
|
|
277
|
+
declare function stripLeakedToolSummary(text: string): string;
|
|
278
|
+
/**
|
|
279
|
+
* Build the `a2a-complete` notify payload. ALWAYS fired for an A2A turn —
|
|
280
|
+
* even when the dispatch produced no relayable text — so the chat service can
|
|
281
|
+
* emit an explicit terminal/stop frame instead of the thread dead-ending
|
|
282
|
+
* silently. `terminated`/`reason` are set when there's nothing to relay
|
|
283
|
+
* (`empty_turn`) or the agent ended the thread via end_conversation
|
|
284
|
+
* (`resolved`).
|
|
285
|
+
*/
|
|
286
|
+
declare function buildA2ACompletePayload<A>(params: {
|
|
287
|
+
conversationId: string;
|
|
288
|
+
fullText: string;
|
|
289
|
+
a2a: A;
|
|
290
|
+
resolved: boolean;
|
|
291
|
+
}): {
|
|
292
|
+
conversationId: string;
|
|
293
|
+
fullText: string;
|
|
294
|
+
a2a: A;
|
|
295
|
+
resolved: boolean;
|
|
296
|
+
terminated?: boolean;
|
|
297
|
+
reason?: string;
|
|
298
|
+
};
|
|
277
299
|
interface PluginApi {
|
|
278
300
|
logger: PluginLogger;
|
|
279
301
|
registrationMode?: 'full' | 'setup-only' | 'setup-runtime' | 'cli-metadata';
|
|
@@ -306,4 +328,4 @@ declare const plugin: {
|
|
|
306
328
|
deactivate(api: PluginApi): Promise<void>;
|
|
307
329
|
};
|
|
308
330
|
//#endregion
|
|
309
|
-
export {
|
|
331
|
+
export { AlfeChannelConfig as a, createAlfeChannelPlugin as i, plugin as n, AlfePluginConfig as o, stripLeakedToolSummary as r, AlfeResolvedAccount as s, buildA2ACompletePayload as t };
|
package/dist/plugin.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { plugin as default };
|
|
1
|
+
import { n as plugin, r as stripLeakedToolSummary, t as buildA2ACompletePayload } from "./plugin2.js";
|
|
2
|
+
export { buildA2ACompletePayload, plugin as default, stripLeakedToolSummary };
|
package/dist/plugin2.cjs
CHANGED
|
@@ -382,6 +382,31 @@ async function listSessions(filters, limit = 50) {
|
|
|
382
382
|
}
|
|
383
383
|
//#endregion
|
|
384
384
|
//#region src/a2a-tools.ts
|
|
385
|
+
let conversationEndRequested = false;
|
|
386
|
+
let a2aTurnArmed = false;
|
|
387
|
+
/**
|
|
388
|
+
* Clear the end signal AND arm the gate. Call immediately before an A2A
|
|
389
|
+
* dispatch. Arming is what permits `end_conversation` to set the flag for the
|
|
390
|
+
* duration of this turn.
|
|
391
|
+
*/
|
|
392
|
+
function resetA2AEndSignal() {
|
|
393
|
+
conversationEndRequested = false;
|
|
394
|
+
a2aTurnArmed = true;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Clear the end signal AND disarm the gate. Call after an A2A dispatch (in a
|
|
398
|
+
* `finally`, so it also runs on error). Disarming closes the gate so a later
|
|
399
|
+
* NON-A2A turn invoking `end_conversation` can't set a flag the next A2A turn
|
|
400
|
+
* would read.
|
|
401
|
+
*/
|
|
402
|
+
function disarmA2AEndSignal() {
|
|
403
|
+
conversationEndRequested = false;
|
|
404
|
+
a2aTurnArmed = false;
|
|
405
|
+
}
|
|
406
|
+
/** True if `end_conversation` was invoked since the last reset. */
|
|
407
|
+
function isA2AEndSignalled() {
|
|
408
|
+
return conversationEndRequested;
|
|
409
|
+
}
|
|
385
410
|
function ok(result) {
|
|
386
411
|
return { content: [{
|
|
387
412
|
type: "text",
|
|
@@ -478,6 +503,7 @@ function buildA2ATools(chatClient, log) {
|
|
|
478
503
|
handler: (params) => {
|
|
479
504
|
const summary = params.summary;
|
|
480
505
|
log.info(`end_conversation tool called${summary ? `: ${summary}` : ""}`);
|
|
506
|
+
if (a2aTurnArmed) conversationEndRequested = true;
|
|
481
507
|
return Promise.resolve({
|
|
482
508
|
status: "conversation_ended",
|
|
483
509
|
summary
|
|
@@ -604,6 +630,99 @@ function validateAttachmentUrl(input, opts = {}) {
|
|
|
604
630
|
*/
|
|
605
631
|
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
606
632
|
const pkg = require$1("../package.json");
|
|
633
|
+
function asString(v) {
|
|
634
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Extract a thinking delta from an OpenClaw `thinking`-stream event.
|
|
638
|
+
* OpenClaw thinking events carry the reasoning text on one of a few
|
|
639
|
+
* fields depending on version; we accept `text` / `delta` / `content`.
|
|
640
|
+
* Returns null when there's nothing to forward.
|
|
641
|
+
*/
|
|
642
|
+
function buildThinkingActivity(data) {
|
|
643
|
+
const text = asString(data.delta) ?? asString(data.text) ?? asString(data.content) ?? asString(data.message?.text);
|
|
644
|
+
if (!text) return null;
|
|
645
|
+
return {
|
|
646
|
+
kind: "thinking",
|
|
647
|
+
text,
|
|
648
|
+
delta: true
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Extract structured tool status from an OpenClaw `tool`-stream event.
|
|
653
|
+
* We send the emoji-free raw `name` plus a lifecycle `status` and let the
|
|
654
|
+
* web render the `🛠️ <Label>: <summary> ✓/✗` row — never OpenClaw's raw
|
|
655
|
+
* `(+N steps)` string. `summary` carries OpenClaw's short detail when the
|
|
656
|
+
* event already provides one (title/summary/label/description), trimmed.
|
|
657
|
+
*/
|
|
658
|
+
function buildToolActivity(data) {
|
|
659
|
+
const toolCallId = asString(data.toolCallId) ?? asString(data.id) ?? asString(data.callId);
|
|
660
|
+
const name = asString(data.name) ?? asString(data.tool) ?? asString(data.toolName);
|
|
661
|
+
if (!toolCallId || !name) return null;
|
|
662
|
+
const phase = asString(data.phase) ?? asString(data.state) ?? asString(data.status);
|
|
663
|
+
const rawStatus = (phase ?? "").toLowerCase();
|
|
664
|
+
let status;
|
|
665
|
+
if (rawStatus === "error" || rawStatus === "failed" || data.error != null || data.isError === true) status = "failed";
|
|
666
|
+
else if (rawStatus === "end" || rawStatus === "done" || rawStatus === "success" || rawStatus === "complete") status = "done";
|
|
667
|
+
else status = "running";
|
|
668
|
+
const summaryRaw = asString(data.summary) ?? asString(data.title) ?? asString(data.label) ?? asString(data.description);
|
|
669
|
+
const summary = summaryRaw ? summaryRaw.slice(0, 200) : void 0;
|
|
670
|
+
return {
|
|
671
|
+
kind: "tool",
|
|
672
|
+
toolCallId,
|
|
673
|
+
name,
|
|
674
|
+
...phase ? { phase } : {},
|
|
675
|
+
status,
|
|
676
|
+
...summary ? { summary } : {}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Workstream C, item 5: strip OpenClaw's leaked tool-summary console line
|
|
681
|
+
* from assistant `responseText` before it's stored / notified as a chat
|
|
682
|
+
* bubble. OpenClaw upstream composes a `⚠️ 🛠️ … (+N steps) failed` summary
|
|
683
|
+
* into the assistant text for a non-zero-exit tool run; `failed` there is a
|
|
684
|
+
* TOOL exit status, not a chat-level failure, and it now has a proper
|
|
685
|
+
* structured tool row (see buildToolActivity). This is a BOUNDED, clearly
|
|
686
|
+
* scoped strip of that one known pattern — it must not touch normal prose.
|
|
687
|
+
*
|
|
688
|
+
* Pattern anatomy (per the plan + observed string):
|
|
689
|
+
* ⚠️ 🛠️ <pipeline text> (+8 steps) failed
|
|
690
|
+
* ⚠️ 🛠️ <pipeline text> (+1 step) failed
|
|
691
|
+
* We match a line that LEADS with OpenClaw's warning/tool emoji (`⚠️` or
|
|
692
|
+
* `🛠️`) — REQUIRED, because OpenClaw always generates the leaked line with
|
|
693
|
+
* it — and contains the `(+<N> step[s])` marker followed by a trailing
|
|
694
|
+
* `failed`/`succeeded`/`done` word. The emoji requirement keeps legitimate
|
|
695
|
+
* assistant prose that merely ends in "(+3 steps) done." from being
|
|
696
|
+
* stripped. Only whole matching lines are removed; everything else is
|
|
697
|
+
* intact. Per-line, line-anchored, non-global.
|
|
698
|
+
*/
|
|
699
|
+
const LEAKED_TOOL_SUMMARY_LINE = /^\s*(?:⚠️|🛠️).*\(\+\d+\s+steps?\)\s*(?:failed|succeeded|done)\.?\s*$/iu;
|
|
700
|
+
function stripLeakedToolSummary(text) {
|
|
701
|
+
if (!text.includes("step")) return text;
|
|
702
|
+
return text.split("\n").filter((line) => !LEAKED_TOOL_SUMMARY_LINE.test(line)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Build the `a2a-complete` notify payload. ALWAYS fired for an A2A turn —
|
|
706
|
+
* even when the dispatch produced no relayable text — so the chat service can
|
|
707
|
+
* emit an explicit terminal/stop frame instead of the thread dead-ending
|
|
708
|
+
* silently. `terminated`/`reason` are set when there's nothing to relay
|
|
709
|
+
* (`empty_turn`) or the agent ended the thread via end_conversation
|
|
710
|
+
* (`resolved`).
|
|
711
|
+
*/
|
|
712
|
+
function buildA2ACompletePayload(params) {
|
|
713
|
+
const hasText = params.fullText.trim().length > 0;
|
|
714
|
+
const terminal = params.resolved || !hasText;
|
|
715
|
+
return {
|
|
716
|
+
conversationId: params.conversationId,
|
|
717
|
+
fullText: params.fullText,
|
|
718
|
+
a2a: params.a2a,
|
|
719
|
+
resolved: params.resolved,
|
|
720
|
+
...terminal ? {
|
|
721
|
+
terminated: true,
|
|
722
|
+
reason: params.resolved ? "resolved" : "empty_turn"
|
|
723
|
+
} : {}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
607
726
|
let dispatchInbound = null;
|
|
608
727
|
/**
|
|
609
728
|
* Resolve OpenClaw SDK functions from the running process.
|
|
@@ -707,6 +826,12 @@ async function downloadAttachments(attachments, log) {
|
|
|
707
826
|
}
|
|
708
827
|
return results;
|
|
709
828
|
}
|
|
829
|
+
let agentTurnQueue = Promise.resolve();
|
|
830
|
+
function enqueueAgentTurn(task) {
|
|
831
|
+
const run = agentTurnQueue.then(task, task);
|
|
832
|
+
agentTurnQueue = run.then(() => void 0, () => void 0);
|
|
833
|
+
return run;
|
|
834
|
+
}
|
|
710
835
|
async function handleAgentRequest(request, log) {
|
|
711
836
|
const runtime = pluginRuntime;
|
|
712
837
|
if (!runtime) {
|
|
@@ -731,16 +856,48 @@ async function handleAgentRequest(request, log) {
|
|
|
731
856
|
let resolvedOpenClawKey = null;
|
|
732
857
|
const unsubscribe = runtime.events.onAgentEvent((evt) => {
|
|
733
858
|
if (!evt.sessionKey) return;
|
|
734
|
-
if (evt.stream
|
|
735
|
-
resolvedOpenClawKey ??= evt.sessionKey;
|
|
859
|
+
if (evt.stream === "assistant" || evt.stream === "thinking" || evt.stream === "tool") resolvedOpenClawKey ??= evt.sessionKey;
|
|
736
860
|
if (evt.sessionKey !== resolvedOpenClawKey) return;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
861
|
+
if (evt.stream === "assistant") {
|
|
862
|
+
chatClient?.sendEvent("chat", {
|
|
863
|
+
runId: evt.runId,
|
|
864
|
+
sessionKey: legacySessionKey,
|
|
865
|
+
seq: evt.seq,
|
|
866
|
+
state: "delta",
|
|
867
|
+
message: evt.data
|
|
868
|
+
});
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (evt.stream === "thinking") {
|
|
872
|
+
const activity = buildThinkingActivity(evt.data);
|
|
873
|
+
if (activity) chatClient?.sendEvent("chat-activity", {
|
|
874
|
+
runId: evt.runId,
|
|
875
|
+
sessionKey: legacySessionKey,
|
|
876
|
+
seq: evt.seq,
|
|
877
|
+
state: "activity",
|
|
878
|
+
activity: {
|
|
879
|
+
...activity,
|
|
880
|
+
ts: evt.ts,
|
|
881
|
+
seq: evt.seq
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (evt.stream === "tool") {
|
|
887
|
+
const activity = buildToolActivity(evt.data);
|
|
888
|
+
if (activity) chatClient?.sendEvent("chat-activity", {
|
|
889
|
+
runId: evt.runId,
|
|
890
|
+
sessionKey: legacySessionKey,
|
|
891
|
+
seq: evt.seq,
|
|
892
|
+
state: "activity",
|
|
893
|
+
activity: {
|
|
894
|
+
...activity,
|
|
895
|
+
ts: evt.ts,
|
|
896
|
+
seq: evt.seq
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
744
901
|
});
|
|
745
902
|
try {
|
|
746
903
|
const downloadedFiles = rawAttachments?.length ? await downloadAttachments(rawAttachments, log) : [];
|
|
@@ -756,7 +913,7 @@ async function handleAgentRequest(request, log) {
|
|
|
756
913
|
const userLabel = displayName ?? userId ?? senderId;
|
|
757
914
|
const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
|
|
758
915
|
let a2aResponseBuffer = "";
|
|
759
|
-
|
|
916
|
+
if (isA2A) resetA2AEndSignal();
|
|
760
917
|
resolvedOpenClawKey = (await dispatchInbound({
|
|
761
918
|
cfg,
|
|
762
919
|
runtime: { channel: runtime.channel },
|
|
@@ -796,19 +953,17 @@ async function handleAgentRequest(request, log) {
|
|
|
796
953
|
`This is an agent-to-agent conversation with ${a2a.sourceAgentName}.`,
|
|
797
954
|
"Rules:",
|
|
798
955
|
"- Only respond if you have new information, a question, or an action to coordinate.",
|
|
799
|
-
"-
|
|
956
|
+
"- When the discussion is complete, call the end_conversation() tool AND end your final message with the literal token [RESOLVED] (uppercase, in square brackets, as the last thing in the message).",
|
|
957
|
+
"- Do NOT write the word \"resolved\" in prose unless you mean to end — only the exact token [RESOLVED] ends the thread.",
|
|
800
958
|
"- Do NOT respond just to acknowledge — that creates infinite loops."
|
|
801
959
|
].join("\n")
|
|
802
960
|
} : {}
|
|
803
961
|
},
|
|
804
962
|
deliver: async (payload) => {
|
|
805
|
-
const responseText = payload.text ?? "";
|
|
963
|
+
const responseText = stripLeakedToolSummary(payload.text ?? "");
|
|
806
964
|
const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
|
|
807
965
|
await addMessage(sessionId, "assistant", responseText);
|
|
808
|
-
if (isA2A)
|
|
809
|
-
a2aResponseBuffer += responseText;
|
|
810
|
-
if (responseText.includes("\"status\":\"conversation_ended\"") || responseText.includes("\"status\": \"conversation_ended\"")) a2aResolved = true;
|
|
811
|
-
}
|
|
966
|
+
if (isA2A) a2aResponseBuffer += responseText;
|
|
812
967
|
chatClient?.notify("agent-message", {
|
|
813
968
|
conversationId: conversationId ?? legacySessionKey,
|
|
814
969
|
text: responseText,
|
|
@@ -824,12 +979,15 @@ async function handleAgentRequest(request, log) {
|
|
|
824
979
|
log.error(`Dispatch error (${info.kind}): ${err instanceof Error ? err.message : String(err)}`);
|
|
825
980
|
}
|
|
826
981
|
})).route.sessionKey;
|
|
827
|
-
if (isA2A
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
982
|
+
if (isA2A) {
|
|
983
|
+
const a2aResolved = isA2AEndSignalled();
|
|
984
|
+
chatClient?.notify("a2a-complete", buildA2ACompletePayload({
|
|
985
|
+
conversationId: conversationId ?? legacySessionKey,
|
|
986
|
+
fullText: a2aResponseBuffer,
|
|
987
|
+
a2a,
|
|
988
|
+
resolved: a2aResolved
|
|
989
|
+
}));
|
|
990
|
+
}
|
|
833
991
|
chatClient?.sendResponse(request.id, true, { sessionKey: resolvedOpenClawKey });
|
|
834
992
|
log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
|
|
835
993
|
} catch (err) {
|
|
@@ -838,6 +996,7 @@ async function handleAgentRequest(request, log) {
|
|
|
838
996
|
chatClient?.sendResponse(request.id, false, { message: errMsg });
|
|
839
997
|
} finally {
|
|
840
998
|
unsubscribe();
|
|
999
|
+
if (isA2A) disarmA2AEndSignal();
|
|
841
1000
|
}
|
|
842
1001
|
}
|
|
843
1002
|
async function handleSessionsList(request, log) {
|
|
@@ -930,7 +1089,7 @@ const plugin = {
|
|
|
930
1089
|
apiKey,
|
|
931
1090
|
onRequest: (request) => {
|
|
932
1091
|
const handle = async () => {
|
|
933
|
-
if (request.method === "agent") await handleAgentRequest(request, log);
|
|
1092
|
+
if (request.method === "agent") await enqueueAgentTurn(() => handleAgentRequest(request, log));
|
|
934
1093
|
else if (request.method === "sessions.list") await handleSessionsList(request, log);
|
|
935
1094
|
else if (request.method === "sessions.get") await handleSessionsGet(request, log);
|
|
936
1095
|
else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
|
|
@@ -1065,6 +1224,12 @@ const plugin = {
|
|
|
1065
1224
|
}
|
|
1066
1225
|
};
|
|
1067
1226
|
//#endregion
|
|
1227
|
+
Object.defineProperty(exports, "buildA2ACompletePayload", {
|
|
1228
|
+
enumerable: true,
|
|
1229
|
+
get: function() {
|
|
1230
|
+
return buildA2ACompletePayload;
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1068
1233
|
Object.defineProperty(exports, "createAlfeChannelPlugin", {
|
|
1069
1234
|
enumerable: true,
|
|
1070
1235
|
get: function() {
|
|
@@ -1077,3 +1242,9 @@ Object.defineProperty(exports, "plugin", {
|
|
|
1077
1242
|
return plugin;
|
|
1078
1243
|
}
|
|
1079
1244
|
});
|
|
1245
|
+
Object.defineProperty(exports, "stripLeakedToolSummary", {
|
|
1246
|
+
enumerable: true,
|
|
1247
|
+
get: function() {
|
|
1248
|
+
return stripLeakedToolSummary;
|
|
1249
|
+
}
|
|
1250
|
+
});
|
package/dist/plugin2.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { plugin as default };
|
|
1
|
+
import { n as plugin, r as stripLeakedToolSummary, t as buildA2ACompletePayload } from "./plugin.cjs";
|
|
2
|
+
export { buildA2ACompletePayload, plugin as default, stripLeakedToolSummary };
|
package/dist/plugin2.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { plugin as default };
|
|
1
|
+
import { n as plugin, r as stripLeakedToolSummary, t as buildA2ACompletePayload } from "./plugin.js";
|
|
2
|
+
export { buildA2ACompletePayload, plugin as default, stripLeakedToolSummary };
|
package/dist/plugin2.js
CHANGED
|
@@ -382,6 +382,31 @@ async function listSessions(filters, limit = 50) {
|
|
|
382
382
|
}
|
|
383
383
|
//#endregion
|
|
384
384
|
//#region src/a2a-tools.ts
|
|
385
|
+
let conversationEndRequested = false;
|
|
386
|
+
let a2aTurnArmed = false;
|
|
387
|
+
/**
|
|
388
|
+
* Clear the end signal AND arm the gate. Call immediately before an A2A
|
|
389
|
+
* dispatch. Arming is what permits `end_conversation` to set the flag for the
|
|
390
|
+
* duration of this turn.
|
|
391
|
+
*/
|
|
392
|
+
function resetA2AEndSignal() {
|
|
393
|
+
conversationEndRequested = false;
|
|
394
|
+
a2aTurnArmed = true;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Clear the end signal AND disarm the gate. Call after an A2A dispatch (in a
|
|
398
|
+
* `finally`, so it also runs on error). Disarming closes the gate so a later
|
|
399
|
+
* NON-A2A turn invoking `end_conversation` can't set a flag the next A2A turn
|
|
400
|
+
* would read.
|
|
401
|
+
*/
|
|
402
|
+
function disarmA2AEndSignal() {
|
|
403
|
+
conversationEndRequested = false;
|
|
404
|
+
a2aTurnArmed = false;
|
|
405
|
+
}
|
|
406
|
+
/** True if `end_conversation` was invoked since the last reset. */
|
|
407
|
+
function isA2AEndSignalled() {
|
|
408
|
+
return conversationEndRequested;
|
|
409
|
+
}
|
|
385
410
|
function ok(result) {
|
|
386
411
|
return { content: [{
|
|
387
412
|
type: "text",
|
|
@@ -478,6 +503,7 @@ function buildA2ATools(chatClient, log) {
|
|
|
478
503
|
handler: (params) => {
|
|
479
504
|
const summary = params.summary;
|
|
480
505
|
log.info(`end_conversation tool called${summary ? `: ${summary}` : ""}`);
|
|
506
|
+
if (a2aTurnArmed) conversationEndRequested = true;
|
|
481
507
|
return Promise.resolve({
|
|
482
508
|
status: "conversation_ended",
|
|
483
509
|
summary
|
|
@@ -604,6 +630,99 @@ function validateAttachmentUrl(input, opts = {}) {
|
|
|
604
630
|
*/
|
|
605
631
|
const require = createRequire(import.meta.url);
|
|
606
632
|
const pkg = require("../package.json");
|
|
633
|
+
function asString(v) {
|
|
634
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Extract a thinking delta from an OpenClaw `thinking`-stream event.
|
|
638
|
+
* OpenClaw thinking events carry the reasoning text on one of a few
|
|
639
|
+
* fields depending on version; we accept `text` / `delta` / `content`.
|
|
640
|
+
* Returns null when there's nothing to forward.
|
|
641
|
+
*/
|
|
642
|
+
function buildThinkingActivity(data) {
|
|
643
|
+
const text = asString(data.delta) ?? asString(data.text) ?? asString(data.content) ?? asString(data.message?.text);
|
|
644
|
+
if (!text) return null;
|
|
645
|
+
return {
|
|
646
|
+
kind: "thinking",
|
|
647
|
+
text,
|
|
648
|
+
delta: true
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Extract structured tool status from an OpenClaw `tool`-stream event.
|
|
653
|
+
* We send the emoji-free raw `name` plus a lifecycle `status` and let the
|
|
654
|
+
* web render the `🛠️ <Label>: <summary> ✓/✗` row — never OpenClaw's raw
|
|
655
|
+
* `(+N steps)` string. `summary` carries OpenClaw's short detail when the
|
|
656
|
+
* event already provides one (title/summary/label/description), trimmed.
|
|
657
|
+
*/
|
|
658
|
+
function buildToolActivity(data) {
|
|
659
|
+
const toolCallId = asString(data.toolCallId) ?? asString(data.id) ?? asString(data.callId);
|
|
660
|
+
const name = asString(data.name) ?? asString(data.tool) ?? asString(data.toolName);
|
|
661
|
+
if (!toolCallId || !name) return null;
|
|
662
|
+
const phase = asString(data.phase) ?? asString(data.state) ?? asString(data.status);
|
|
663
|
+
const rawStatus = (phase ?? "").toLowerCase();
|
|
664
|
+
let status;
|
|
665
|
+
if (rawStatus === "error" || rawStatus === "failed" || data.error != null || data.isError === true) status = "failed";
|
|
666
|
+
else if (rawStatus === "end" || rawStatus === "done" || rawStatus === "success" || rawStatus === "complete") status = "done";
|
|
667
|
+
else status = "running";
|
|
668
|
+
const summaryRaw = asString(data.summary) ?? asString(data.title) ?? asString(data.label) ?? asString(data.description);
|
|
669
|
+
const summary = summaryRaw ? summaryRaw.slice(0, 200) : void 0;
|
|
670
|
+
return {
|
|
671
|
+
kind: "tool",
|
|
672
|
+
toolCallId,
|
|
673
|
+
name,
|
|
674
|
+
...phase ? { phase } : {},
|
|
675
|
+
status,
|
|
676
|
+
...summary ? { summary } : {}
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Workstream C, item 5: strip OpenClaw's leaked tool-summary console line
|
|
681
|
+
* from assistant `responseText` before it's stored / notified as a chat
|
|
682
|
+
* bubble. OpenClaw upstream composes a `⚠️ 🛠️ … (+N steps) failed` summary
|
|
683
|
+
* into the assistant text for a non-zero-exit tool run; `failed` there is a
|
|
684
|
+
* TOOL exit status, not a chat-level failure, and it now has a proper
|
|
685
|
+
* structured tool row (see buildToolActivity). This is a BOUNDED, clearly
|
|
686
|
+
* scoped strip of that one known pattern — it must not touch normal prose.
|
|
687
|
+
*
|
|
688
|
+
* Pattern anatomy (per the plan + observed string):
|
|
689
|
+
* ⚠️ 🛠️ <pipeline text> (+8 steps) failed
|
|
690
|
+
* ⚠️ 🛠️ <pipeline text> (+1 step) failed
|
|
691
|
+
* We match a line that LEADS with OpenClaw's warning/tool emoji (`⚠️` or
|
|
692
|
+
* `🛠️`) — REQUIRED, because OpenClaw always generates the leaked line with
|
|
693
|
+
* it — and contains the `(+<N> step[s])` marker followed by a trailing
|
|
694
|
+
* `failed`/`succeeded`/`done` word. The emoji requirement keeps legitimate
|
|
695
|
+
* assistant prose that merely ends in "(+3 steps) done." from being
|
|
696
|
+
* stripped. Only whole matching lines are removed; everything else is
|
|
697
|
+
* intact. Per-line, line-anchored, non-global.
|
|
698
|
+
*/
|
|
699
|
+
const LEAKED_TOOL_SUMMARY_LINE = /^\s*(?:⚠️|🛠️).*\(\+\d+\s+steps?\)\s*(?:failed|succeeded|done)\.?\s*$/iu;
|
|
700
|
+
function stripLeakedToolSummary(text) {
|
|
701
|
+
if (!text.includes("step")) return text;
|
|
702
|
+
return text.split("\n").filter((line) => !LEAKED_TOOL_SUMMARY_LINE.test(line)).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Build the `a2a-complete` notify payload. ALWAYS fired for an A2A turn —
|
|
706
|
+
* even when the dispatch produced no relayable text — so the chat service can
|
|
707
|
+
* emit an explicit terminal/stop frame instead of the thread dead-ending
|
|
708
|
+
* silently. `terminated`/`reason` are set when there's nothing to relay
|
|
709
|
+
* (`empty_turn`) or the agent ended the thread via end_conversation
|
|
710
|
+
* (`resolved`).
|
|
711
|
+
*/
|
|
712
|
+
function buildA2ACompletePayload(params) {
|
|
713
|
+
const hasText = params.fullText.trim().length > 0;
|
|
714
|
+
const terminal = params.resolved || !hasText;
|
|
715
|
+
return {
|
|
716
|
+
conversationId: params.conversationId,
|
|
717
|
+
fullText: params.fullText,
|
|
718
|
+
a2a: params.a2a,
|
|
719
|
+
resolved: params.resolved,
|
|
720
|
+
...terminal ? {
|
|
721
|
+
terminated: true,
|
|
722
|
+
reason: params.resolved ? "resolved" : "empty_turn"
|
|
723
|
+
} : {}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
607
726
|
let dispatchInbound = null;
|
|
608
727
|
/**
|
|
609
728
|
* Resolve OpenClaw SDK functions from the running process.
|
|
@@ -707,6 +826,12 @@ async function downloadAttachments(attachments, log) {
|
|
|
707
826
|
}
|
|
708
827
|
return results;
|
|
709
828
|
}
|
|
829
|
+
let agentTurnQueue = Promise.resolve();
|
|
830
|
+
function enqueueAgentTurn(task) {
|
|
831
|
+
const run = agentTurnQueue.then(task, task);
|
|
832
|
+
agentTurnQueue = run.then(() => void 0, () => void 0);
|
|
833
|
+
return run;
|
|
834
|
+
}
|
|
710
835
|
async function handleAgentRequest(request, log) {
|
|
711
836
|
const runtime = pluginRuntime;
|
|
712
837
|
if (!runtime) {
|
|
@@ -731,16 +856,48 @@ async function handleAgentRequest(request, log) {
|
|
|
731
856
|
let resolvedOpenClawKey = null;
|
|
732
857
|
const unsubscribe = runtime.events.onAgentEvent((evt) => {
|
|
733
858
|
if (!evt.sessionKey) return;
|
|
734
|
-
if (evt.stream
|
|
735
|
-
resolvedOpenClawKey ??= evt.sessionKey;
|
|
859
|
+
if (evt.stream === "assistant" || evt.stream === "thinking" || evt.stream === "tool") resolvedOpenClawKey ??= evt.sessionKey;
|
|
736
860
|
if (evt.sessionKey !== resolvedOpenClawKey) return;
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
861
|
+
if (evt.stream === "assistant") {
|
|
862
|
+
chatClient?.sendEvent("chat", {
|
|
863
|
+
runId: evt.runId,
|
|
864
|
+
sessionKey: legacySessionKey,
|
|
865
|
+
seq: evt.seq,
|
|
866
|
+
state: "delta",
|
|
867
|
+
message: evt.data
|
|
868
|
+
});
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (evt.stream === "thinking") {
|
|
872
|
+
const activity = buildThinkingActivity(evt.data);
|
|
873
|
+
if (activity) chatClient?.sendEvent("chat-activity", {
|
|
874
|
+
runId: evt.runId,
|
|
875
|
+
sessionKey: legacySessionKey,
|
|
876
|
+
seq: evt.seq,
|
|
877
|
+
state: "activity",
|
|
878
|
+
activity: {
|
|
879
|
+
...activity,
|
|
880
|
+
ts: evt.ts,
|
|
881
|
+
seq: evt.seq
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (evt.stream === "tool") {
|
|
887
|
+
const activity = buildToolActivity(evt.data);
|
|
888
|
+
if (activity) chatClient?.sendEvent("chat-activity", {
|
|
889
|
+
runId: evt.runId,
|
|
890
|
+
sessionKey: legacySessionKey,
|
|
891
|
+
seq: evt.seq,
|
|
892
|
+
state: "activity",
|
|
893
|
+
activity: {
|
|
894
|
+
...activity,
|
|
895
|
+
ts: evt.ts,
|
|
896
|
+
seq: evt.seq
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
744
901
|
});
|
|
745
902
|
try {
|
|
746
903
|
const downloadedFiles = rawAttachments?.length ? await downloadAttachments(rawAttachments, log) : [];
|
|
@@ -756,7 +913,7 @@ async function handleAgentRequest(request, log) {
|
|
|
756
913
|
const userLabel = displayName ?? userId ?? senderId;
|
|
757
914
|
const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
|
|
758
915
|
let a2aResponseBuffer = "";
|
|
759
|
-
|
|
916
|
+
if (isA2A) resetA2AEndSignal();
|
|
760
917
|
resolvedOpenClawKey = (await dispatchInbound({
|
|
761
918
|
cfg,
|
|
762
919
|
runtime: { channel: runtime.channel },
|
|
@@ -796,19 +953,17 @@ async function handleAgentRequest(request, log) {
|
|
|
796
953
|
`This is an agent-to-agent conversation with ${a2a.sourceAgentName}.`,
|
|
797
954
|
"Rules:",
|
|
798
955
|
"- Only respond if you have new information, a question, or an action to coordinate.",
|
|
799
|
-
"-
|
|
956
|
+
"- When the discussion is complete, call the end_conversation() tool AND end your final message with the literal token [RESOLVED] (uppercase, in square brackets, as the last thing in the message).",
|
|
957
|
+
"- Do NOT write the word \"resolved\" in prose unless you mean to end — only the exact token [RESOLVED] ends the thread.",
|
|
800
958
|
"- Do NOT respond just to acknowledge — that creates infinite loops."
|
|
801
959
|
].join("\n")
|
|
802
960
|
} : {}
|
|
803
961
|
},
|
|
804
962
|
deliver: async (payload) => {
|
|
805
|
-
const responseText = payload.text ?? "";
|
|
963
|
+
const responseText = stripLeakedToolSummary(payload.text ?? "");
|
|
806
964
|
const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
|
|
807
965
|
await addMessage(sessionId, "assistant", responseText);
|
|
808
|
-
if (isA2A)
|
|
809
|
-
a2aResponseBuffer += responseText;
|
|
810
|
-
if (responseText.includes("\"status\":\"conversation_ended\"") || responseText.includes("\"status\": \"conversation_ended\"")) a2aResolved = true;
|
|
811
|
-
}
|
|
966
|
+
if (isA2A) a2aResponseBuffer += responseText;
|
|
812
967
|
chatClient?.notify("agent-message", {
|
|
813
968
|
conversationId: conversationId ?? legacySessionKey,
|
|
814
969
|
text: responseText,
|
|
@@ -824,12 +979,15 @@ async function handleAgentRequest(request, log) {
|
|
|
824
979
|
log.error(`Dispatch error (${info.kind}): ${err instanceof Error ? err.message : String(err)}`);
|
|
825
980
|
}
|
|
826
981
|
})).route.sessionKey;
|
|
827
|
-
if (isA2A
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
982
|
+
if (isA2A) {
|
|
983
|
+
const a2aResolved = isA2AEndSignalled();
|
|
984
|
+
chatClient?.notify("a2a-complete", buildA2ACompletePayload({
|
|
985
|
+
conversationId: conversationId ?? legacySessionKey,
|
|
986
|
+
fullText: a2aResponseBuffer,
|
|
987
|
+
a2a,
|
|
988
|
+
resolved: a2aResolved
|
|
989
|
+
}));
|
|
990
|
+
}
|
|
833
991
|
chatClient?.sendResponse(request.id, true, { sessionKey: resolvedOpenClawKey });
|
|
834
992
|
log.info(`Agent dispatch complete: sessionKey=${resolvedOpenClawKey}`);
|
|
835
993
|
} catch (err) {
|
|
@@ -838,6 +996,7 @@ async function handleAgentRequest(request, log) {
|
|
|
838
996
|
chatClient?.sendResponse(request.id, false, { message: errMsg });
|
|
839
997
|
} finally {
|
|
840
998
|
unsubscribe();
|
|
999
|
+
if (isA2A) disarmA2AEndSignal();
|
|
841
1000
|
}
|
|
842
1001
|
}
|
|
843
1002
|
async function handleSessionsList(request, log) {
|
|
@@ -930,7 +1089,7 @@ const plugin = {
|
|
|
930
1089
|
apiKey,
|
|
931
1090
|
onRequest: (request) => {
|
|
932
1091
|
const handle = async () => {
|
|
933
|
-
if (request.method === "agent") await handleAgentRequest(request, log);
|
|
1092
|
+
if (request.method === "agent") await enqueueAgentTurn(() => handleAgentRequest(request, log));
|
|
934
1093
|
else if (request.method === "sessions.list") await handleSessionsList(request, log);
|
|
935
1094
|
else if (request.method === "sessions.get") await handleSessionsGet(request, log);
|
|
936
1095
|
else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
|
|
@@ -1065,4 +1224,4 @@ const plugin = {
|
|
|
1065
1224
|
}
|
|
1066
1225
|
};
|
|
1067
1226
|
//#endregion
|
|
1068
|
-
export { createAlfeChannelPlugin as
|
|
1227
|
+
export { createAlfeChannelPlugin as i, plugin as n, stripLeakedToolSummary as r, buildA2ACompletePayload as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/openclaw-chat",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "OpenClaw chat plugin for Alfe — web widget and mobile app channels",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/plugin.js",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"openclaw.plugin.json"
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@alfe.ai/agent-api-client": "^0.2.
|
|
31
|
-
"@alfe.ai/chat": "^0.0.
|
|
30
|
+
"@alfe.ai/agent-api-client": "^0.2.2",
|
|
31
|
+
"@alfe.ai/chat": "^0.0.10",
|
|
32
32
|
"@alfe.ai/config": "^0.0.9"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|