@alfe.ai/openclaw-chat 0.3.3 → 0.4.0
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 +32 -1
- package/dist/index.d.ts +32 -1
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +2 -0
- package/dist/plugin.d.cts +16 -1
- package/dist/plugin.d.ts +16 -1
- package/dist/plugin.js +2 -2
- package/dist/plugin2.cjs +254 -21
- package/dist/plugin2.d.cts +2 -2
- package/dist/plugin2.d.ts +2 -2
- package/dist/plugin2.js +243 -22
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { d as AlfePluginConfig, f as AlfeResolvedAccount, l as createAlfeChannelPlugin, o as plugin, u as AlfeChannelConfig } from "./plugin.cjs";
|
|
2
2
|
|
|
3
3
|
//#region src/session-store.d.ts
|
|
4
4
|
|
|
@@ -19,6 +19,35 @@ interface ChatMessage {
|
|
|
19
19
|
senderId?: string;
|
|
20
20
|
senderName?: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Persisted form of a turn's agent activity (tool cards + thinking), so a
|
|
24
|
+
* refresh can replay what the live `chat-activity` stream showed. Terminal
|
|
25
|
+
* by construction: no `progressText` (transient streaming window) and no
|
|
26
|
+
* `running` status — a tool still running at flush time is persisted as
|
|
27
|
+
* `interrupted`. Tool text fields arrive pre-capped (activity-serialize.ts);
|
|
28
|
+
* thinking text is capped at append time (MAX_ACTIVITY_THINKING_CHARS).
|
|
29
|
+
*/
|
|
30
|
+
type ChatActivityRecord = {
|
|
31
|
+
kind: 'thinking';
|
|
32
|
+
text: string;
|
|
33
|
+
ts: number;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'tool';
|
|
36
|
+
toolCallId: string;
|
|
37
|
+
name: string;
|
|
38
|
+
status: 'done' | 'failed' | 'interrupted';
|
|
39
|
+
summary?: string;
|
|
40
|
+
argsText?: string;
|
|
41
|
+
resultText?: string;
|
|
42
|
+
isError?: boolean;
|
|
43
|
+
durationMs?: number;
|
|
44
|
+
truncated?: {
|
|
45
|
+
args?: boolean;
|
|
46
|
+
progress?: boolean;
|
|
47
|
+
result?: boolean;
|
|
48
|
+
};
|
|
49
|
+
ts: number;
|
|
50
|
+
};
|
|
22
51
|
interface SessionData {
|
|
23
52
|
sessionId: string;
|
|
24
53
|
agentId: string;
|
|
@@ -28,6 +57,8 @@ interface SessionData {
|
|
|
28
57
|
createdAt: string;
|
|
29
58
|
updatedAt: string;
|
|
30
59
|
messages: ChatMessage[];
|
|
60
|
+
/** Absent on session files written before activity persistence shipped. */
|
|
61
|
+
activity?: ChatActivityRecord[];
|
|
31
62
|
}
|
|
32
63
|
interface SessionSummary {
|
|
33
64
|
sessionId: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { d as AlfePluginConfig, f as AlfeResolvedAccount, l as createAlfeChannelPlugin, o as plugin, u as AlfeChannelConfig } from "./plugin.js";
|
|
2
2
|
|
|
3
3
|
//#region src/session-store.d.ts
|
|
4
4
|
|
|
@@ -19,6 +19,35 @@ interface ChatMessage {
|
|
|
19
19
|
senderId?: string;
|
|
20
20
|
senderName?: string;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Persisted form of a turn's agent activity (tool cards + thinking), so a
|
|
24
|
+
* refresh can replay what the live `chat-activity` stream showed. Terminal
|
|
25
|
+
* by construction: no `progressText` (transient streaming window) and no
|
|
26
|
+
* `running` status — a tool still running at flush time is persisted as
|
|
27
|
+
* `interrupted`. Tool text fields arrive pre-capped (activity-serialize.ts);
|
|
28
|
+
* thinking text is capped at append time (MAX_ACTIVITY_THINKING_CHARS).
|
|
29
|
+
*/
|
|
30
|
+
type ChatActivityRecord = {
|
|
31
|
+
kind: 'thinking';
|
|
32
|
+
text: string;
|
|
33
|
+
ts: number;
|
|
34
|
+
} | {
|
|
35
|
+
kind: 'tool';
|
|
36
|
+
toolCallId: string;
|
|
37
|
+
name: string;
|
|
38
|
+
status: 'done' | 'failed' | 'interrupted';
|
|
39
|
+
summary?: string;
|
|
40
|
+
argsText?: string;
|
|
41
|
+
resultText?: string;
|
|
42
|
+
isError?: boolean;
|
|
43
|
+
durationMs?: number;
|
|
44
|
+
truncated?: {
|
|
45
|
+
args?: boolean;
|
|
46
|
+
progress?: boolean;
|
|
47
|
+
result?: boolean;
|
|
48
|
+
};
|
|
49
|
+
ts: number;
|
|
50
|
+
};
|
|
22
51
|
interface SessionData {
|
|
23
52
|
sessionId: string;
|
|
24
53
|
agentId: string;
|
|
@@ -28,6 +57,8 @@ interface SessionData {
|
|
|
28
57
|
createdAt: string;
|
|
29
58
|
updatedAt: string;
|
|
30
59
|
messages: ChatMessage[];
|
|
60
|
+
/** Absent on session files written before activity persistence shipped. */
|
|
61
|
+
activity?: ChatActivityRecord[];
|
|
31
62
|
}
|
|
32
63
|
interface SessionSummary {
|
|
33
64
|
sessionId: string;
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as plugin, c as createAlfeChannelPlugin } from "./plugin2.js";
|
|
2
2
|
export { createAlfeChannelPlugin, plugin as default };
|
package/dist/plugin.cjs
CHANGED
|
@@ -7,4 +7,6 @@ exports.admitAgentEvent = require_plugin.admitAgentEvent;
|
|
|
7
7
|
exports.buildA2ACompletePayload = require_plugin.buildA2ACompletePayload;
|
|
8
8
|
exports.buildToolActivity = require_plugin.buildToolActivity;
|
|
9
9
|
exports.default = require_plugin.plugin;
|
|
10
|
+
exports.joinAssistantText = require_plugin.joinAssistantText;
|
|
11
|
+
exports.rewriteAssistantText = require_plugin.rewriteAssistantText;
|
|
10
12
|
exports.stripLeakedToolSummary = require_plugin.stripLeakedToolSummary;
|
package/dist/plugin.d.cts
CHANGED
|
@@ -334,6 +334,21 @@ type ChatActivity = {
|
|
|
334
334
|
ts?: number;
|
|
335
335
|
seq?: number;
|
|
336
336
|
};
|
|
337
|
+
/**
|
|
338
|
+
* Join the text parts of an assistant-stream event's content array.
|
|
339
|
+
* Assistant `evt.data` is a message object whose `content` is an array of
|
|
340
|
+
* typed parts; deltas are CUMULATIVE snapshots (the relay length-compares
|
|
341
|
+
* them — see adapter.ts's lastSentLength slicing). Non-array/absent content
|
|
342
|
+
* yields '' (nothing to divert).
|
|
343
|
+
*/
|
|
344
|
+
declare function joinAssistantText(data: Record<string, unknown>): string;
|
|
345
|
+
/**
|
|
346
|
+
* Rebuild an assistant event's message with its text parts replaced by one
|
|
347
|
+
* `{type:'text', text: visible}` part. Non-text parts are preserved — the
|
|
348
|
+
* relay's consumers (adapter/inject/voice-bridge) only join text parts, so
|
|
349
|
+
* relative ordering against non-text items is not load-bearing.
|
|
350
|
+
*/
|
|
351
|
+
declare function rewriteAssistantText(data: Record<string, unknown>, visible: string): Record<string, unknown>;
|
|
337
352
|
/**
|
|
338
353
|
* Extract structured tool status from an OpenClaw `tool`-stream event.
|
|
339
354
|
* We send the emoji-free raw `name` plus a lifecycle `status` and let the
|
|
@@ -396,4 +411,4 @@ declare const plugin: {
|
|
|
396
411
|
deactivate(api: PluginApi): Promise<void>;
|
|
397
412
|
};
|
|
398
413
|
//#endregion
|
|
399
|
-
export {
|
|
414
|
+
export { joinAssistantText as a, stripLeakedToolSummary as c, AlfePluginConfig as d, AlfeResolvedAccount as f, buildToolActivity as i, createAlfeChannelPlugin as l, admitAgentEvent as n, plugin as o, buildA2ACompletePayload as r, rewriteAssistantText as s, RunEventGate as t, AlfeChannelConfig as u };
|
package/dist/plugin.d.ts
CHANGED
|
@@ -334,6 +334,21 @@ type ChatActivity = {
|
|
|
334
334
|
ts?: number;
|
|
335
335
|
seq?: number;
|
|
336
336
|
};
|
|
337
|
+
/**
|
|
338
|
+
* Join the text parts of an assistant-stream event's content array.
|
|
339
|
+
* Assistant `evt.data` is a message object whose `content` is an array of
|
|
340
|
+
* typed parts; deltas are CUMULATIVE snapshots (the relay length-compares
|
|
341
|
+
* them — see adapter.ts's lastSentLength slicing). Non-array/absent content
|
|
342
|
+
* yields '' (nothing to divert).
|
|
343
|
+
*/
|
|
344
|
+
declare function joinAssistantText(data: Record<string, unknown>): string;
|
|
345
|
+
/**
|
|
346
|
+
* Rebuild an assistant event's message with its text parts replaced by one
|
|
347
|
+
* `{type:'text', text: visible}` part. Non-text parts are preserved — the
|
|
348
|
+
* relay's consumers (adapter/inject/voice-bridge) only join text parts, so
|
|
349
|
+
* relative ordering against non-text items is not load-bearing.
|
|
350
|
+
*/
|
|
351
|
+
declare function rewriteAssistantText(data: Record<string, unknown>, visible: string): Record<string, unknown>;
|
|
337
352
|
/**
|
|
338
353
|
* Extract structured tool status from an OpenClaw `tool`-stream event.
|
|
339
354
|
* We send the emoji-free raw `name` plus a lifecycle `status` and let the
|
|
@@ -396,4 +411,4 @@ declare const plugin: {
|
|
|
396
411
|
deactivate(api: PluginApi): Promise<void>;
|
|
397
412
|
};
|
|
398
413
|
//#endregion
|
|
399
|
-
export {
|
|
414
|
+
export { joinAssistantText as a, stripLeakedToolSummary as c, AlfePluginConfig as d, AlfeResolvedAccount as f, buildToolActivity as i, createAlfeChannelPlugin as l, admitAgentEvent as n, plugin as o, buildA2ACompletePayload as r, rewriteAssistantText as s, RunEventGate as t, AlfeChannelConfig as u };
|
package/dist/plugin.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
|
|
1
|
+
import { a as plugin, i as joinAssistantText, n as buildA2ACompletePayload, o as rewriteAssistantText, r as buildToolActivity, s as stripLeakedToolSummary, t as admitAgentEvent } from "./plugin2.js";
|
|
2
|
+
export { admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, rewriteAssistantText, stripLeakedToolSummary };
|
package/dist/plugin2.cjs
CHANGED
|
@@ -344,6 +344,31 @@ async function addMessage(sessionId, role, content, senderId, senderName) {
|
|
|
344
344
|
writeLocks.set(sessionId, next);
|
|
345
345
|
await next;
|
|
346
346
|
}
|
|
347
|
+
const MAX_ACTIVITY_PER_SESSION = 500;
|
|
348
|
+
const MAX_ACTIVITY_THINKING_CHARS = 16e3;
|
|
349
|
+
/**
|
|
350
|
+
* Append a turn's terminalized activity records. Called ONCE per turn (from
|
|
351
|
+
* the dispatch `finally`) — never per-delta: thinking deltas are
|
|
352
|
+
* high-frequency and each save rewrites the whole session file.
|
|
353
|
+
*/
|
|
354
|
+
async function appendActivity(sessionId, entries) {
|
|
355
|
+
if (entries.length === 0) return;
|
|
356
|
+
const next = (writeLocks.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
357
|
+
const session = await getSession(sessionId);
|
|
358
|
+
if (!session) return;
|
|
359
|
+
const capped = entries.map((e) => e.kind === "thinking" && e.text.length > MAX_ACTIVITY_THINKING_CHARS ? {
|
|
360
|
+
...e,
|
|
361
|
+
text: e.text.slice(0, MAX_ACTIVITY_THINKING_CHARS)
|
|
362
|
+
} : e);
|
|
363
|
+
const combined = [...session.activity ?? [], ...capped];
|
|
364
|
+
session.activity = combined.length > MAX_ACTIVITY_PER_SESSION ? combined.slice(combined.length - MAX_ACTIVITY_PER_SESSION) : combined;
|
|
365
|
+
await saveSession(session);
|
|
366
|
+
}).finally(() => {
|
|
367
|
+
if (writeLocks.get(sessionId) === next) writeLocks.delete(sessionId);
|
|
368
|
+
});
|
|
369
|
+
writeLocks.set(sessionId, next);
|
|
370
|
+
await next;
|
|
371
|
+
}
|
|
347
372
|
async function listSessions(filters, limit = 50) {
|
|
348
373
|
await ensureDir();
|
|
349
374
|
let files;
|
|
@@ -848,6 +873,101 @@ function extractResultText(result) {
|
|
|
848
873
|
return parts.join("\n");
|
|
849
874
|
}
|
|
850
875
|
//#endregion
|
|
876
|
+
//#region src/think-tags.ts
|
|
877
|
+
/**
|
|
878
|
+
* Inline `<think>` span handling for assistant text.
|
|
879
|
+
*
|
|
880
|
+
* Reasoning models routed through the AI proxy (DeepSeek `deepseek-*`,
|
|
881
|
+
* Zhipu `glm-*`, …) emit their reasoning INLINE as `<think>…</think>` inside
|
|
882
|
+
* the normal content stream instead of a structured thinking channel. The
|
|
883
|
+
* openclaw-chat plugin (0.4.0+) diverts those spans into thinking activity
|
|
884
|
+
* frames at the source; this relay-side copy is the DEFENSIVE layer for
|
|
885
|
+
* agents still running older plugin versions — leaked reasoning is dropped
|
|
886
|
+
* here, never synthesized into activity (that's the plugin's job).
|
|
887
|
+
*
|
|
888
|
+
* KEEP IN SYNC (byte-identical): this module exists at BOTH
|
|
889
|
+
* `services/chat/src/lib/think-tags.ts` (relay, defensive layer)
|
|
890
|
+
* `packages/openclaw-chat/src/think-tags.ts` (plugin, at-source divert)
|
|
891
|
+
* The published plugin cannot import service internals, so the module is
|
|
892
|
+
* duplicated, same as the ChatActivity shape.
|
|
893
|
+
*
|
|
894
|
+
* Known accepted limitation: a literal `<think>` in legitimate prose or a
|
|
895
|
+
* code fence is treated as reasoning and stripped/diverted.
|
|
896
|
+
*/
|
|
897
|
+
const OPEN_TAG = "<think>";
|
|
898
|
+
const CLOSE_TAG = "</think>";
|
|
899
|
+
/**
|
|
900
|
+
* Split CUMULATIVE assistant text into the user-visible part and the
|
|
901
|
+
* reasoning part.
|
|
902
|
+
*
|
|
903
|
+
* Invariant (load-bearing): for successive cumulative snapshots that grow by
|
|
904
|
+
* appending, `visible` is monotonically append-only — `visible(n)` is always
|
|
905
|
+
* a prefix of `visible(n + 1)`. The delta relays slice by a `lastSentLength`
|
|
906
|
+
* offset into this string; if `visible` ever shrank or rewrote earlier
|
|
907
|
+
* characters, clients would receive corrupted deltas. This holds because the
|
|
908
|
+
* scan is deterministic left-to-right and a trailing PARTIAL tag (any strict
|
|
909
|
+
* prefix of the next expected tag, e.g. `"<thi"` or `"</th"`) is HELD BACK
|
|
910
|
+
* from both outputs until later text disambiguates it.
|
|
911
|
+
*/
|
|
912
|
+
function splitThinkTags(cumulative) {
|
|
913
|
+
let visible = "";
|
|
914
|
+
let thinking = "";
|
|
915
|
+
let inThink = false;
|
|
916
|
+
let i = 0;
|
|
917
|
+
const n = cumulative.length;
|
|
918
|
+
while (i < n) {
|
|
919
|
+
const tag = inThink ? CLOSE_TAG : OPEN_TAG;
|
|
920
|
+
const idx = cumulative.indexOf(tag, i);
|
|
921
|
+
if (idx !== -1) {
|
|
922
|
+
const chunk = cumulative.slice(i, idx);
|
|
923
|
+
if (inThink) thinking += chunk;
|
|
924
|
+
else visible += chunk;
|
|
925
|
+
inThink = !inThink;
|
|
926
|
+
i = idx + tag.length;
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
let rest = cumulative.slice(i);
|
|
930
|
+
const held = trailingPartialTagLength(rest, tag);
|
|
931
|
+
if (held > 0) rest = rest.slice(0, rest.length - held);
|
|
932
|
+
if (inThink) thinking += rest;
|
|
933
|
+
else visible += rest;
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
visible,
|
|
938
|
+
thinking
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
/** Length of the longest strict prefix of `tag` that is a suffix of `text`. */
|
|
942
|
+
function trailingPartialTagLength(text, tag) {
|
|
943
|
+
const max = Math.min(text.length, tag.length - 1);
|
|
944
|
+
for (let len = max; len > 0; len--) if (text.endsWith(tag.slice(0, len))) return len;
|
|
945
|
+
return 0;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Strip `<think>…</think>` spans (and an unclosed trailing `<think>…`) from
|
|
949
|
+
* FINAL text, then tidy the whitespace the spans leave behind.
|
|
950
|
+
*
|
|
951
|
+
* Idempotent — `stripThinkSpans(stripThinkSpans(x)) === stripThinkSpans(x)`.
|
|
952
|
+
* New plugin + new relay both strip, so the second pass must be a no-op; the
|
|
953
|
+
* fixpoint loop also covers pathological inputs where removing one span
|
|
954
|
+
* makes the surrounding fragments form a new one.
|
|
955
|
+
*
|
|
956
|
+
* NOT for sliced/streaming paths — the trim + blank-line collapse breaks
|
|
957
|
+
* prefix alignment with `lastSentLength`; those paths use
|
|
958
|
+
* `splitThinkTags(...).visible` instead.
|
|
959
|
+
*/
|
|
960
|
+
function stripThinkSpans(text) {
|
|
961
|
+
let out = text;
|
|
962
|
+
let prev;
|
|
963
|
+
do {
|
|
964
|
+
prev = out;
|
|
965
|
+
out = out.replace(/<think>[\s\S]*?<\/think>/g, "");
|
|
966
|
+
} while (out !== prev);
|
|
967
|
+
out = out.replace(/<think>[\s\S]*$/, "");
|
|
968
|
+
return out.replace(/\n{3,}/g, "\n\n").trim();
|
|
969
|
+
}
|
|
970
|
+
//#endregion
|
|
851
971
|
//#region src/plugin.ts
|
|
852
972
|
/**
|
|
853
973
|
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
@@ -913,7 +1033,7 @@ function asString(v) {
|
|
|
913
1033
|
* Returns null when there's nothing to forward.
|
|
914
1034
|
*/
|
|
915
1035
|
function buildThinkingActivity(data) {
|
|
916
|
-
const text = asString(data.delta) ?? asString(data.text) ?? asString(data.content) ?? asString(data.message?.text);
|
|
1036
|
+
const text = asString(data.delta) ?? asString(data.text) ?? asString(data.content) ?? asString(data.reasoning) ?? asString(data.reasoning_content) ?? asString(data.message?.text);
|
|
917
1037
|
if (!text) return null;
|
|
918
1038
|
return {
|
|
919
1039
|
kind: "thinking",
|
|
@@ -922,6 +1042,34 @@ function buildThinkingActivity(data) {
|
|
|
922
1042
|
};
|
|
923
1043
|
}
|
|
924
1044
|
/**
|
|
1045
|
+
* Join the text parts of an assistant-stream event's content array.
|
|
1046
|
+
* Assistant `evt.data` is a message object whose `content` is an array of
|
|
1047
|
+
* typed parts; deltas are CUMULATIVE snapshots (the relay length-compares
|
|
1048
|
+
* them — see adapter.ts's lastSentLength slicing). Non-array/absent content
|
|
1049
|
+
* yields '' (nothing to divert).
|
|
1050
|
+
*/
|
|
1051
|
+
function joinAssistantText(data) {
|
|
1052
|
+
const content = data.content;
|
|
1053
|
+
if (!Array.isArray(content)) return "";
|
|
1054
|
+
return content.filter((c) => typeof c === "object" && c !== null && c.type === "text" && typeof c.text === "string").map((c) => c.text).join("");
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Rebuild an assistant event's message with its text parts replaced by one
|
|
1058
|
+
* `{type:'text', text: visible}` part. Non-text parts are preserved — the
|
|
1059
|
+
* relay's consumers (adapter/inject/voice-bridge) only join text parts, so
|
|
1060
|
+
* relative ordering against non-text items is not load-bearing.
|
|
1061
|
+
*/
|
|
1062
|
+
function rewriteAssistantText(data, visible) {
|
|
1063
|
+
const nonText = (Array.isArray(data.content) ? data.content : []).filter((c) => !(typeof c === "object" && c !== null && c.type === "text"));
|
|
1064
|
+
return {
|
|
1065
|
+
...data,
|
|
1066
|
+
content: [...nonText, {
|
|
1067
|
+
type: "text",
|
|
1068
|
+
text: visible
|
|
1069
|
+
}]
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
925
1073
|
* Extract structured tool status from an OpenClaw `tool`-stream event.
|
|
926
1074
|
* We send the emoji-free raw `name` plus a lifecycle `status` and let the
|
|
927
1075
|
* web render the `🛠️ <Label>: <summary> ✓/✗` row — never OpenClaw's raw
|
|
@@ -1171,6 +1319,43 @@ async function handleAgentRequest(request, log) {
|
|
|
1171
1319
|
runId: null,
|
|
1172
1320
|
sessionKey: null
|
|
1173
1321
|
};
|
|
1322
|
+
let lastThinkingLen = 0;
|
|
1323
|
+
const turnThinking = {
|
|
1324
|
+
text: "",
|
|
1325
|
+
ts: 0
|
|
1326
|
+
};
|
|
1327
|
+
const turnTools = /* @__PURE__ */ new Map();
|
|
1328
|
+
const recordThinking = (text, ts) => {
|
|
1329
|
+
if (!turnThinking.text) turnThinking.ts = ts;
|
|
1330
|
+
turnThinking.text += text;
|
|
1331
|
+
};
|
|
1332
|
+
const recordTool = (activity, ts) => {
|
|
1333
|
+
const prev = turnTools.get(activity.toolCallId);
|
|
1334
|
+
turnTools.set(activity.toolCallId, {
|
|
1335
|
+
kind: "tool",
|
|
1336
|
+
toolCallId: activity.toolCallId,
|
|
1337
|
+
name: activity.name,
|
|
1338
|
+
status: activity.status === "running" ? prev?.status ?? "interrupted" : activity.status,
|
|
1339
|
+
...activity.summary ?? prev?.summary ? { summary: activity.summary ?? prev?.summary } : {},
|
|
1340
|
+
...prev?.argsText ?? activity.argsText ? { argsText: prev?.argsText ?? activity.argsText } : {},
|
|
1341
|
+
...activity.resultText ?? prev?.resultText ? { resultText: activity.resultText ?? prev?.resultText } : {},
|
|
1342
|
+
...(activity.isError ?? prev?.isError) !== void 0 ? { isError: activity.isError ?? prev?.isError } : {},
|
|
1343
|
+
...(activity.durationMs ?? prev?.durationMs) !== void 0 ? { durationMs: activity.durationMs ?? prev?.durationMs } : {},
|
|
1344
|
+
...activity.truncated ?? prev?.truncated ? { truncated: activity.truncated ?? prev?.truncated } : {},
|
|
1345
|
+
ts: prev?.ts ?? ts
|
|
1346
|
+
});
|
|
1347
|
+
};
|
|
1348
|
+
const flushTurnActivity = async () => {
|
|
1349
|
+
const entries = [];
|
|
1350
|
+
if (turnThinking.text) entries.push({
|
|
1351
|
+
kind: "thinking",
|
|
1352
|
+
text: turnThinking.text,
|
|
1353
|
+
ts: turnThinking.ts
|
|
1354
|
+
});
|
|
1355
|
+
for (const tool of turnTools.values()) entries.push(tool);
|
|
1356
|
+
entries.sort((a, b) => a.ts - b.ts);
|
|
1357
|
+
await appendActivity(sessionId, entries);
|
|
1358
|
+
};
|
|
1174
1359
|
const UPDATE_THROTTLE_MS = 250;
|
|
1175
1360
|
const pendingUpdates = /* @__PURE__ */ new Map();
|
|
1176
1361
|
const flushToolUpdate = (toolCallId) => {
|
|
@@ -1187,33 +1372,58 @@ async function handleAgentRequest(request, log) {
|
|
|
1187
1372
|
const unsubscribe = runtime.events.onAgentEvent((evt) => {
|
|
1188
1373
|
if (!admitAgentEvent(runGate, evt)) return;
|
|
1189
1374
|
if (evt.stream === "assistant") {
|
|
1375
|
+
const raw = joinAssistantText(evt.data);
|
|
1376
|
+
const { visible, thinking } = splitThinkTags(raw);
|
|
1377
|
+
if (thinking.length > lastThinkingLen) {
|
|
1378
|
+
const thinkingDelta = thinking.slice(lastThinkingLen);
|
|
1379
|
+
chatClient?.sendEvent("chat-activity", {
|
|
1380
|
+
runId: evt.runId,
|
|
1381
|
+
sessionKey: legacySessionKey,
|
|
1382
|
+
seq: evt.seq,
|
|
1383
|
+
state: "activity",
|
|
1384
|
+
activity: {
|
|
1385
|
+
kind: "thinking",
|
|
1386
|
+
text: thinkingDelta,
|
|
1387
|
+
delta: true,
|
|
1388
|
+
ts: evt.ts,
|
|
1389
|
+
seq: evt.seq
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
recordThinking(thinkingDelta, evt.ts);
|
|
1393
|
+
lastThinkingLen = thinking.length;
|
|
1394
|
+
}
|
|
1395
|
+
const message = visible === raw && thinking.length === 0 ? evt.data : rewriteAssistantText(evt.data, visible);
|
|
1190
1396
|
chatClient?.sendEvent("chat", {
|
|
1191
1397
|
runId: evt.runId,
|
|
1192
1398
|
sessionKey: legacySessionKey,
|
|
1193
1399
|
seq: evt.seq,
|
|
1194
1400
|
state: "delta",
|
|
1195
|
-
message
|
|
1401
|
+
message
|
|
1196
1402
|
});
|
|
1197
1403
|
return;
|
|
1198
1404
|
}
|
|
1199
1405
|
if (evt.stream === "thinking") {
|
|
1200
1406
|
const activity = buildThinkingActivity(evt.data);
|
|
1201
|
-
if (activity)
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1407
|
+
if (activity) {
|
|
1408
|
+
chatClient?.sendEvent("chat-activity", {
|
|
1409
|
+
runId: evt.runId,
|
|
1410
|
+
sessionKey: legacySessionKey,
|
|
1411
|
+
seq: evt.seq,
|
|
1412
|
+
state: "activity",
|
|
1413
|
+
activity: {
|
|
1414
|
+
...activity,
|
|
1415
|
+
ts: evt.ts,
|
|
1416
|
+
seq: evt.seq
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
if (activity.kind === "thinking") recordThinking(activity.text, evt.ts);
|
|
1420
|
+
}
|
|
1212
1421
|
return;
|
|
1213
1422
|
}
|
|
1214
1423
|
if (evt.stream === "tool") {
|
|
1215
1424
|
const activity = buildToolActivity(evt.data);
|
|
1216
1425
|
if (activity?.kind !== "tool") return;
|
|
1426
|
+
recordTool(activity, evt.ts);
|
|
1217
1427
|
const toolCallId = activity.toolCallId;
|
|
1218
1428
|
const send = () => {
|
|
1219
1429
|
chatClient?.sendEvent("chat-activity", {
|
|
@@ -1309,10 +1519,10 @@ async function handleAgentRequest(request, log) {
|
|
|
1309
1519
|
} : {}
|
|
1310
1520
|
},
|
|
1311
1521
|
deliver: async (payload) => {
|
|
1312
|
-
const responseText = stripLeakedToolSummary(payload.text ?? "");
|
|
1522
|
+
const responseText = stripLeakedToolSummary(stripThinkSpans(payload.text ?? ""));
|
|
1313
1523
|
const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
|
|
1314
1524
|
await addMessage(sessionId, "assistant", responseText);
|
|
1315
|
-
if (isA2A) a2aResponseBuffer += responseText;
|
|
1525
|
+
if (isA2A) a2aResponseBuffer += (a2aResponseBuffer && responseText ? "\n\n" : "") + responseText;
|
|
1316
1526
|
chatClient?.notify("agent-message", {
|
|
1317
1527
|
conversationId: conversationId ?? legacySessionKey,
|
|
1318
1528
|
text: responseText,
|
|
@@ -1346,6 +1556,11 @@ async function handleAgentRequest(request, log) {
|
|
|
1346
1556
|
} finally {
|
|
1347
1557
|
unsubscribe();
|
|
1348
1558
|
clearAllToolUpdates();
|
|
1559
|
+
try {
|
|
1560
|
+
await flushTurnActivity();
|
|
1561
|
+
} catch (err) {
|
|
1562
|
+
log.error(`Activity persistence failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1563
|
+
}
|
|
1349
1564
|
if (isA2A) disarmA2AEndSignal();
|
|
1350
1565
|
}
|
|
1351
1566
|
}
|
|
@@ -1371,14 +1586,16 @@ async function handleSessionsGet(request, log) {
|
|
|
1371
1586
|
if (!session) {
|
|
1372
1587
|
chatClient?.sendResponse(request.id, true, {
|
|
1373
1588
|
ok: false,
|
|
1374
|
-
error: "Session not found"
|
|
1589
|
+
error: "Session not found",
|
|
1590
|
+
messages: []
|
|
1375
1591
|
});
|
|
1376
1592
|
return;
|
|
1377
1593
|
}
|
|
1378
1594
|
if (params.userId && session.userId && session.userId !== params.userId) {
|
|
1379
1595
|
chatClient?.sendResponse(request.id, true, {
|
|
1380
1596
|
ok: false,
|
|
1381
|
-
error: "Session not found"
|
|
1597
|
+
error: "Session not found",
|
|
1598
|
+
messages: []
|
|
1382
1599
|
});
|
|
1383
1600
|
return;
|
|
1384
1601
|
}
|
|
@@ -1392,7 +1609,8 @@ async function handleSessionsGet(request, log) {
|
|
|
1392
1609
|
role: m.role,
|
|
1393
1610
|
content: m.content,
|
|
1394
1611
|
timestamp: m.timestamp
|
|
1395
|
-
}))
|
|
1612
|
+
})),
|
|
1613
|
+
activity: (session.activity ?? []).map((a) => ({ ...a }))
|
|
1396
1614
|
});
|
|
1397
1615
|
} catch (err) {
|
|
1398
1616
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -1500,11 +1718,13 @@ const plugin = {
|
|
|
1500
1718
|
const session = await getSession(params.sessionId);
|
|
1501
1719
|
if (!session) return {
|
|
1502
1720
|
ok: false,
|
|
1503
|
-
error: "Session not found"
|
|
1721
|
+
error: "Session not found",
|
|
1722
|
+
messages: []
|
|
1504
1723
|
};
|
|
1505
1724
|
if (params.userId && session.userId && session.userId !== params.userId) return {
|
|
1506
1725
|
ok: false,
|
|
1507
|
-
error: "Session not found"
|
|
1726
|
+
error: "Session not found",
|
|
1727
|
+
messages: []
|
|
1508
1728
|
};
|
|
1509
1729
|
return {
|
|
1510
1730
|
sessionId: session.sessionId,
|
|
@@ -1516,7 +1736,8 @@ const plugin = {
|
|
|
1516
1736
|
role: m.role,
|
|
1517
1737
|
content: m.content,
|
|
1518
1738
|
timestamp: m.timestamp
|
|
1519
|
-
}))
|
|
1739
|
+
})),
|
|
1740
|
+
activity: (session.activity ?? []).map((a) => ({ ...a }))
|
|
1520
1741
|
};
|
|
1521
1742
|
});
|
|
1522
1743
|
gw.__alfeChatGatewayMethodsRegistered = true;
|
|
@@ -1598,12 +1819,24 @@ Object.defineProperty(exports, "createAlfeChannelPlugin", {
|
|
|
1598
1819
|
return createAlfeChannelPlugin;
|
|
1599
1820
|
}
|
|
1600
1821
|
});
|
|
1822
|
+
Object.defineProperty(exports, "joinAssistantText", {
|
|
1823
|
+
enumerable: true,
|
|
1824
|
+
get: function() {
|
|
1825
|
+
return joinAssistantText;
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1601
1828
|
Object.defineProperty(exports, "plugin", {
|
|
1602
1829
|
enumerable: true,
|
|
1603
1830
|
get: function() {
|
|
1604
1831
|
return plugin;
|
|
1605
1832
|
}
|
|
1606
1833
|
});
|
|
1834
|
+
Object.defineProperty(exports, "rewriteAssistantText", {
|
|
1835
|
+
enumerable: true,
|
|
1836
|
+
get: function() {
|
|
1837
|
+
return rewriteAssistantText;
|
|
1838
|
+
}
|
|
1839
|
+
});
|
|
1607
1840
|
Object.defineProperty(exports, "stripLeakedToolSummary", {
|
|
1608
1841
|
enumerable: true,
|
|
1609
1842
|
get: function() {
|
package/dist/plugin2.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { RunEventGate, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
|
|
1
|
+
import { a as joinAssistantText, c as stripLeakedToolSummary, i as buildToolActivity, n as admitAgentEvent, o as plugin, r as buildA2ACompletePayload, s as rewriteAssistantText, t as RunEventGate } from "./plugin.cjs";
|
|
2
|
+
export { RunEventGate, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, rewriteAssistantText, stripLeakedToolSummary };
|
package/dist/plugin2.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { RunEventGate, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
|
|
1
|
+
import { a as joinAssistantText, c as stripLeakedToolSummary, i as buildToolActivity, n as admitAgentEvent, o as plugin, r as buildA2ACompletePayload, s as rewriteAssistantText, t as RunEventGate } from "./plugin.js";
|
|
2
|
+
export { RunEventGate, admitAgentEvent, buildA2ACompletePayload, buildToolActivity, plugin as default, joinAssistantText, rewriteAssistantText, stripLeakedToolSummary };
|
package/dist/plugin2.js
CHANGED
|
@@ -344,6 +344,31 @@ async function addMessage(sessionId, role, content, senderId, senderName) {
|
|
|
344
344
|
writeLocks.set(sessionId, next);
|
|
345
345
|
await next;
|
|
346
346
|
}
|
|
347
|
+
const MAX_ACTIVITY_PER_SESSION = 500;
|
|
348
|
+
const MAX_ACTIVITY_THINKING_CHARS = 16e3;
|
|
349
|
+
/**
|
|
350
|
+
* Append a turn's terminalized activity records. Called ONCE per turn (from
|
|
351
|
+
* the dispatch `finally`) — never per-delta: thinking deltas are
|
|
352
|
+
* high-frequency and each save rewrites the whole session file.
|
|
353
|
+
*/
|
|
354
|
+
async function appendActivity(sessionId, entries) {
|
|
355
|
+
if (entries.length === 0) return;
|
|
356
|
+
const next = (writeLocks.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
357
|
+
const session = await getSession(sessionId);
|
|
358
|
+
if (!session) return;
|
|
359
|
+
const capped = entries.map((e) => e.kind === "thinking" && e.text.length > MAX_ACTIVITY_THINKING_CHARS ? {
|
|
360
|
+
...e,
|
|
361
|
+
text: e.text.slice(0, MAX_ACTIVITY_THINKING_CHARS)
|
|
362
|
+
} : e);
|
|
363
|
+
const combined = [...session.activity ?? [], ...capped];
|
|
364
|
+
session.activity = combined.length > MAX_ACTIVITY_PER_SESSION ? combined.slice(combined.length - MAX_ACTIVITY_PER_SESSION) : combined;
|
|
365
|
+
await saveSession(session);
|
|
366
|
+
}).finally(() => {
|
|
367
|
+
if (writeLocks.get(sessionId) === next) writeLocks.delete(sessionId);
|
|
368
|
+
});
|
|
369
|
+
writeLocks.set(sessionId, next);
|
|
370
|
+
await next;
|
|
371
|
+
}
|
|
347
372
|
async function listSessions(filters, limit = 50) {
|
|
348
373
|
await ensureDir();
|
|
349
374
|
let files;
|
|
@@ -848,6 +873,101 @@ function extractResultText(result) {
|
|
|
848
873
|
return parts.join("\n");
|
|
849
874
|
}
|
|
850
875
|
//#endregion
|
|
876
|
+
//#region src/think-tags.ts
|
|
877
|
+
/**
|
|
878
|
+
* Inline `<think>` span handling for assistant text.
|
|
879
|
+
*
|
|
880
|
+
* Reasoning models routed through the AI proxy (DeepSeek `deepseek-*`,
|
|
881
|
+
* Zhipu `glm-*`, …) emit their reasoning INLINE as `<think>…</think>` inside
|
|
882
|
+
* the normal content stream instead of a structured thinking channel. The
|
|
883
|
+
* openclaw-chat plugin (0.4.0+) diverts those spans into thinking activity
|
|
884
|
+
* frames at the source; this relay-side copy is the DEFENSIVE layer for
|
|
885
|
+
* agents still running older plugin versions — leaked reasoning is dropped
|
|
886
|
+
* here, never synthesized into activity (that's the plugin's job).
|
|
887
|
+
*
|
|
888
|
+
* KEEP IN SYNC (byte-identical): this module exists at BOTH
|
|
889
|
+
* `services/chat/src/lib/think-tags.ts` (relay, defensive layer)
|
|
890
|
+
* `packages/openclaw-chat/src/think-tags.ts` (plugin, at-source divert)
|
|
891
|
+
* The published plugin cannot import service internals, so the module is
|
|
892
|
+
* duplicated, same as the ChatActivity shape.
|
|
893
|
+
*
|
|
894
|
+
* Known accepted limitation: a literal `<think>` in legitimate prose or a
|
|
895
|
+
* code fence is treated as reasoning and stripped/diverted.
|
|
896
|
+
*/
|
|
897
|
+
const OPEN_TAG = "<think>";
|
|
898
|
+
const CLOSE_TAG = "</think>";
|
|
899
|
+
/**
|
|
900
|
+
* Split CUMULATIVE assistant text into the user-visible part and the
|
|
901
|
+
* reasoning part.
|
|
902
|
+
*
|
|
903
|
+
* Invariant (load-bearing): for successive cumulative snapshots that grow by
|
|
904
|
+
* appending, `visible` is monotonically append-only — `visible(n)` is always
|
|
905
|
+
* a prefix of `visible(n + 1)`. The delta relays slice by a `lastSentLength`
|
|
906
|
+
* offset into this string; if `visible` ever shrank or rewrote earlier
|
|
907
|
+
* characters, clients would receive corrupted deltas. This holds because the
|
|
908
|
+
* scan is deterministic left-to-right and a trailing PARTIAL tag (any strict
|
|
909
|
+
* prefix of the next expected tag, e.g. `"<thi"` or `"</th"`) is HELD BACK
|
|
910
|
+
* from both outputs until later text disambiguates it.
|
|
911
|
+
*/
|
|
912
|
+
function splitThinkTags(cumulative) {
|
|
913
|
+
let visible = "";
|
|
914
|
+
let thinking = "";
|
|
915
|
+
let inThink = false;
|
|
916
|
+
let i = 0;
|
|
917
|
+
const n = cumulative.length;
|
|
918
|
+
while (i < n) {
|
|
919
|
+
const tag = inThink ? CLOSE_TAG : OPEN_TAG;
|
|
920
|
+
const idx = cumulative.indexOf(tag, i);
|
|
921
|
+
if (idx !== -1) {
|
|
922
|
+
const chunk = cumulative.slice(i, idx);
|
|
923
|
+
if (inThink) thinking += chunk;
|
|
924
|
+
else visible += chunk;
|
|
925
|
+
inThink = !inThink;
|
|
926
|
+
i = idx + tag.length;
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
let rest = cumulative.slice(i);
|
|
930
|
+
const held = trailingPartialTagLength(rest, tag);
|
|
931
|
+
if (held > 0) rest = rest.slice(0, rest.length - held);
|
|
932
|
+
if (inThink) thinking += rest;
|
|
933
|
+
else visible += rest;
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
visible,
|
|
938
|
+
thinking
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
/** Length of the longest strict prefix of `tag` that is a suffix of `text`. */
|
|
942
|
+
function trailingPartialTagLength(text, tag) {
|
|
943
|
+
const max = Math.min(text.length, tag.length - 1);
|
|
944
|
+
for (let len = max; len > 0; len--) if (text.endsWith(tag.slice(0, len))) return len;
|
|
945
|
+
return 0;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Strip `<think>…</think>` spans (and an unclosed trailing `<think>…`) from
|
|
949
|
+
* FINAL text, then tidy the whitespace the spans leave behind.
|
|
950
|
+
*
|
|
951
|
+
* Idempotent — `stripThinkSpans(stripThinkSpans(x)) === stripThinkSpans(x)`.
|
|
952
|
+
* New plugin + new relay both strip, so the second pass must be a no-op; the
|
|
953
|
+
* fixpoint loop also covers pathological inputs where removing one span
|
|
954
|
+
* makes the surrounding fragments form a new one.
|
|
955
|
+
*
|
|
956
|
+
* NOT for sliced/streaming paths — the trim + blank-line collapse breaks
|
|
957
|
+
* prefix alignment with `lastSentLength`; those paths use
|
|
958
|
+
* `splitThinkTags(...).visible` instead.
|
|
959
|
+
*/
|
|
960
|
+
function stripThinkSpans(text) {
|
|
961
|
+
let out = text;
|
|
962
|
+
let prev;
|
|
963
|
+
do {
|
|
964
|
+
prev = out;
|
|
965
|
+
out = out.replace(/<think>[\s\S]*?<\/think>/g, "");
|
|
966
|
+
} while (out !== prev);
|
|
967
|
+
out = out.replace(/<think>[\s\S]*$/, "");
|
|
968
|
+
return out.replace(/\n{3,}/g, "\n\n").trim();
|
|
969
|
+
}
|
|
970
|
+
//#endregion
|
|
851
971
|
//#region src/plugin.ts
|
|
852
972
|
/**
|
|
853
973
|
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
@@ -913,7 +1033,7 @@ function asString(v) {
|
|
|
913
1033
|
* Returns null when there's nothing to forward.
|
|
914
1034
|
*/
|
|
915
1035
|
function buildThinkingActivity(data) {
|
|
916
|
-
const text = asString(data.delta) ?? asString(data.text) ?? asString(data.content) ?? asString(data.message?.text);
|
|
1036
|
+
const text = asString(data.delta) ?? asString(data.text) ?? asString(data.content) ?? asString(data.reasoning) ?? asString(data.reasoning_content) ?? asString(data.message?.text);
|
|
917
1037
|
if (!text) return null;
|
|
918
1038
|
return {
|
|
919
1039
|
kind: "thinking",
|
|
@@ -922,6 +1042,34 @@ function buildThinkingActivity(data) {
|
|
|
922
1042
|
};
|
|
923
1043
|
}
|
|
924
1044
|
/**
|
|
1045
|
+
* Join the text parts of an assistant-stream event's content array.
|
|
1046
|
+
* Assistant `evt.data` is a message object whose `content` is an array of
|
|
1047
|
+
* typed parts; deltas are CUMULATIVE snapshots (the relay length-compares
|
|
1048
|
+
* them — see adapter.ts's lastSentLength slicing). Non-array/absent content
|
|
1049
|
+
* yields '' (nothing to divert).
|
|
1050
|
+
*/
|
|
1051
|
+
function joinAssistantText(data) {
|
|
1052
|
+
const content = data.content;
|
|
1053
|
+
if (!Array.isArray(content)) return "";
|
|
1054
|
+
return content.filter((c) => typeof c === "object" && c !== null && c.type === "text" && typeof c.text === "string").map((c) => c.text).join("");
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Rebuild an assistant event's message with its text parts replaced by one
|
|
1058
|
+
* `{type:'text', text: visible}` part. Non-text parts are preserved — the
|
|
1059
|
+
* relay's consumers (adapter/inject/voice-bridge) only join text parts, so
|
|
1060
|
+
* relative ordering against non-text items is not load-bearing.
|
|
1061
|
+
*/
|
|
1062
|
+
function rewriteAssistantText(data, visible) {
|
|
1063
|
+
const nonText = (Array.isArray(data.content) ? data.content : []).filter((c) => !(typeof c === "object" && c !== null && c.type === "text"));
|
|
1064
|
+
return {
|
|
1065
|
+
...data,
|
|
1066
|
+
content: [...nonText, {
|
|
1067
|
+
type: "text",
|
|
1068
|
+
text: visible
|
|
1069
|
+
}]
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
925
1073
|
* Extract structured tool status from an OpenClaw `tool`-stream event.
|
|
926
1074
|
* We send the emoji-free raw `name` plus a lifecycle `status` and let the
|
|
927
1075
|
* web render the `🛠️ <Label>: <summary> ✓/✗` row — never OpenClaw's raw
|
|
@@ -1171,6 +1319,43 @@ async function handleAgentRequest(request, log) {
|
|
|
1171
1319
|
runId: null,
|
|
1172
1320
|
sessionKey: null
|
|
1173
1321
|
};
|
|
1322
|
+
let lastThinkingLen = 0;
|
|
1323
|
+
const turnThinking = {
|
|
1324
|
+
text: "",
|
|
1325
|
+
ts: 0
|
|
1326
|
+
};
|
|
1327
|
+
const turnTools = /* @__PURE__ */ new Map();
|
|
1328
|
+
const recordThinking = (text, ts) => {
|
|
1329
|
+
if (!turnThinking.text) turnThinking.ts = ts;
|
|
1330
|
+
turnThinking.text += text;
|
|
1331
|
+
};
|
|
1332
|
+
const recordTool = (activity, ts) => {
|
|
1333
|
+
const prev = turnTools.get(activity.toolCallId);
|
|
1334
|
+
turnTools.set(activity.toolCallId, {
|
|
1335
|
+
kind: "tool",
|
|
1336
|
+
toolCallId: activity.toolCallId,
|
|
1337
|
+
name: activity.name,
|
|
1338
|
+
status: activity.status === "running" ? prev?.status ?? "interrupted" : activity.status,
|
|
1339
|
+
...activity.summary ?? prev?.summary ? { summary: activity.summary ?? prev?.summary } : {},
|
|
1340
|
+
...prev?.argsText ?? activity.argsText ? { argsText: prev?.argsText ?? activity.argsText } : {},
|
|
1341
|
+
...activity.resultText ?? prev?.resultText ? { resultText: activity.resultText ?? prev?.resultText } : {},
|
|
1342
|
+
...(activity.isError ?? prev?.isError) !== void 0 ? { isError: activity.isError ?? prev?.isError } : {},
|
|
1343
|
+
...(activity.durationMs ?? prev?.durationMs) !== void 0 ? { durationMs: activity.durationMs ?? prev?.durationMs } : {},
|
|
1344
|
+
...activity.truncated ?? prev?.truncated ? { truncated: activity.truncated ?? prev?.truncated } : {},
|
|
1345
|
+
ts: prev?.ts ?? ts
|
|
1346
|
+
});
|
|
1347
|
+
};
|
|
1348
|
+
const flushTurnActivity = async () => {
|
|
1349
|
+
const entries = [];
|
|
1350
|
+
if (turnThinking.text) entries.push({
|
|
1351
|
+
kind: "thinking",
|
|
1352
|
+
text: turnThinking.text,
|
|
1353
|
+
ts: turnThinking.ts
|
|
1354
|
+
});
|
|
1355
|
+
for (const tool of turnTools.values()) entries.push(tool);
|
|
1356
|
+
entries.sort((a, b) => a.ts - b.ts);
|
|
1357
|
+
await appendActivity(sessionId, entries);
|
|
1358
|
+
};
|
|
1174
1359
|
const UPDATE_THROTTLE_MS = 250;
|
|
1175
1360
|
const pendingUpdates = /* @__PURE__ */ new Map();
|
|
1176
1361
|
const flushToolUpdate = (toolCallId) => {
|
|
@@ -1187,33 +1372,58 @@ async function handleAgentRequest(request, log) {
|
|
|
1187
1372
|
const unsubscribe = runtime.events.onAgentEvent((evt) => {
|
|
1188
1373
|
if (!admitAgentEvent(runGate, evt)) return;
|
|
1189
1374
|
if (evt.stream === "assistant") {
|
|
1375
|
+
const raw = joinAssistantText(evt.data);
|
|
1376
|
+
const { visible, thinking } = splitThinkTags(raw);
|
|
1377
|
+
if (thinking.length > lastThinkingLen) {
|
|
1378
|
+
const thinkingDelta = thinking.slice(lastThinkingLen);
|
|
1379
|
+
chatClient?.sendEvent("chat-activity", {
|
|
1380
|
+
runId: evt.runId,
|
|
1381
|
+
sessionKey: legacySessionKey,
|
|
1382
|
+
seq: evt.seq,
|
|
1383
|
+
state: "activity",
|
|
1384
|
+
activity: {
|
|
1385
|
+
kind: "thinking",
|
|
1386
|
+
text: thinkingDelta,
|
|
1387
|
+
delta: true,
|
|
1388
|
+
ts: evt.ts,
|
|
1389
|
+
seq: evt.seq
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
recordThinking(thinkingDelta, evt.ts);
|
|
1393
|
+
lastThinkingLen = thinking.length;
|
|
1394
|
+
}
|
|
1395
|
+
const message = visible === raw && thinking.length === 0 ? evt.data : rewriteAssistantText(evt.data, visible);
|
|
1190
1396
|
chatClient?.sendEvent("chat", {
|
|
1191
1397
|
runId: evt.runId,
|
|
1192
1398
|
sessionKey: legacySessionKey,
|
|
1193
1399
|
seq: evt.seq,
|
|
1194
1400
|
state: "delta",
|
|
1195
|
-
message
|
|
1401
|
+
message
|
|
1196
1402
|
});
|
|
1197
1403
|
return;
|
|
1198
1404
|
}
|
|
1199
1405
|
if (evt.stream === "thinking") {
|
|
1200
1406
|
const activity = buildThinkingActivity(evt.data);
|
|
1201
|
-
if (activity)
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1407
|
+
if (activity) {
|
|
1408
|
+
chatClient?.sendEvent("chat-activity", {
|
|
1409
|
+
runId: evt.runId,
|
|
1410
|
+
sessionKey: legacySessionKey,
|
|
1411
|
+
seq: evt.seq,
|
|
1412
|
+
state: "activity",
|
|
1413
|
+
activity: {
|
|
1414
|
+
...activity,
|
|
1415
|
+
ts: evt.ts,
|
|
1416
|
+
seq: evt.seq
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
if (activity.kind === "thinking") recordThinking(activity.text, evt.ts);
|
|
1420
|
+
}
|
|
1212
1421
|
return;
|
|
1213
1422
|
}
|
|
1214
1423
|
if (evt.stream === "tool") {
|
|
1215
1424
|
const activity = buildToolActivity(evt.data);
|
|
1216
1425
|
if (activity?.kind !== "tool") return;
|
|
1426
|
+
recordTool(activity, evt.ts);
|
|
1217
1427
|
const toolCallId = activity.toolCallId;
|
|
1218
1428
|
const send = () => {
|
|
1219
1429
|
chatClient?.sendEvent("chat-activity", {
|
|
@@ -1309,10 +1519,10 @@ async function handleAgentRequest(request, log) {
|
|
|
1309
1519
|
} : {}
|
|
1310
1520
|
},
|
|
1311
1521
|
deliver: async (payload) => {
|
|
1312
|
-
const responseText = stripLeakedToolSummary(payload.text ?? "");
|
|
1522
|
+
const responseText = stripLeakedToolSummary(stripThinkSpans(payload.text ?? ""));
|
|
1313
1523
|
const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
|
|
1314
1524
|
await addMessage(sessionId, "assistant", responseText);
|
|
1315
|
-
if (isA2A) a2aResponseBuffer += responseText;
|
|
1525
|
+
if (isA2A) a2aResponseBuffer += (a2aResponseBuffer && responseText ? "\n\n" : "") + responseText;
|
|
1316
1526
|
chatClient?.notify("agent-message", {
|
|
1317
1527
|
conversationId: conversationId ?? legacySessionKey,
|
|
1318
1528
|
text: responseText,
|
|
@@ -1346,6 +1556,11 @@ async function handleAgentRequest(request, log) {
|
|
|
1346
1556
|
} finally {
|
|
1347
1557
|
unsubscribe();
|
|
1348
1558
|
clearAllToolUpdates();
|
|
1559
|
+
try {
|
|
1560
|
+
await flushTurnActivity();
|
|
1561
|
+
} catch (err) {
|
|
1562
|
+
log.error(`Activity persistence failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1563
|
+
}
|
|
1349
1564
|
if (isA2A) disarmA2AEndSignal();
|
|
1350
1565
|
}
|
|
1351
1566
|
}
|
|
@@ -1371,14 +1586,16 @@ async function handleSessionsGet(request, log) {
|
|
|
1371
1586
|
if (!session) {
|
|
1372
1587
|
chatClient?.sendResponse(request.id, true, {
|
|
1373
1588
|
ok: false,
|
|
1374
|
-
error: "Session not found"
|
|
1589
|
+
error: "Session not found",
|
|
1590
|
+
messages: []
|
|
1375
1591
|
});
|
|
1376
1592
|
return;
|
|
1377
1593
|
}
|
|
1378
1594
|
if (params.userId && session.userId && session.userId !== params.userId) {
|
|
1379
1595
|
chatClient?.sendResponse(request.id, true, {
|
|
1380
1596
|
ok: false,
|
|
1381
|
-
error: "Session not found"
|
|
1597
|
+
error: "Session not found",
|
|
1598
|
+
messages: []
|
|
1382
1599
|
});
|
|
1383
1600
|
return;
|
|
1384
1601
|
}
|
|
@@ -1392,7 +1609,8 @@ async function handleSessionsGet(request, log) {
|
|
|
1392
1609
|
role: m.role,
|
|
1393
1610
|
content: m.content,
|
|
1394
1611
|
timestamp: m.timestamp
|
|
1395
|
-
}))
|
|
1612
|
+
})),
|
|
1613
|
+
activity: (session.activity ?? []).map((a) => ({ ...a }))
|
|
1396
1614
|
});
|
|
1397
1615
|
} catch (err) {
|
|
1398
1616
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -1500,11 +1718,13 @@ const plugin = {
|
|
|
1500
1718
|
const session = await getSession(params.sessionId);
|
|
1501
1719
|
if (!session) return {
|
|
1502
1720
|
ok: false,
|
|
1503
|
-
error: "Session not found"
|
|
1721
|
+
error: "Session not found",
|
|
1722
|
+
messages: []
|
|
1504
1723
|
};
|
|
1505
1724
|
if (params.userId && session.userId && session.userId !== params.userId) return {
|
|
1506
1725
|
ok: false,
|
|
1507
|
-
error: "Session not found"
|
|
1726
|
+
error: "Session not found",
|
|
1727
|
+
messages: []
|
|
1508
1728
|
};
|
|
1509
1729
|
return {
|
|
1510
1730
|
sessionId: session.sessionId,
|
|
@@ -1516,7 +1736,8 @@ const plugin = {
|
|
|
1516
1736
|
role: m.role,
|
|
1517
1737
|
content: m.content,
|
|
1518
1738
|
timestamp: m.timestamp
|
|
1519
|
-
}))
|
|
1739
|
+
})),
|
|
1740
|
+
activity: (session.activity ?? []).map((a) => ({ ...a }))
|
|
1520
1741
|
};
|
|
1521
1742
|
});
|
|
1522
1743
|
gw.__alfeChatGatewayMethodsRegistered = true;
|
|
@@ -1574,4 +1795,4 @@ const plugin = {
|
|
|
1574
1795
|
}
|
|
1575
1796
|
};
|
|
1576
1797
|
//#endregion
|
|
1577
|
-
export {
|
|
1798
|
+
export { plugin as a, createAlfeChannelPlugin as c, joinAssistantText as i, buildA2ACompletePayload as n, rewriteAssistantText as o, buildToolActivity as r, stripLeakedToolSummary as s, admitAgentEvent as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/openclaw-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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.5.0",
|
|
31
30
|
"@alfe.ai/chat": "^0.0.14",
|
|
31
|
+
"@alfe.ai/agent-api-client": "^0.5.0",
|
|
32
32
|
"@alfe.ai/config": "^0.3.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|