@alfe.ai/openclaw-chat 0.3.3 → 0.5.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 +6 -0
- package/dist/plugin.d.cts +45 -1
- package/dist/plugin.d.ts +45 -1
- package/dist/plugin.js +2 -2
- package/dist/plugin2.cjs +456 -46
- package/dist/plugin2.d.cts +2 -2
- package/dist/plugin2.d.ts +2 -2
- package/dist/plugin2.js +421 -47
- package/package.json +1 -1
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
|
|
@@ -1038,6 +1186,37 @@ function buildA2ACompletePayload(params) {
|
|
|
1038
1186
|
};
|
|
1039
1187
|
}
|
|
1040
1188
|
let dispatchInbound = null;
|
|
1189
|
+
let abortEmbeddedAgentRun = null;
|
|
1190
|
+
let resolveActiveRunSessionId = null;
|
|
1191
|
+
let resolveRouteEnvelope = null;
|
|
1192
|
+
/**
|
|
1193
|
+
* Load the OpenClaw SDK surfaces from a single resolver anchor. Returns true
|
|
1194
|
+
* once the critical `dispatchInboundDirectDmWithRuntime` is resolved. The abort
|
|
1195
|
+
* primitives + route resolver are best-effort — an older openclaw without them
|
|
1196
|
+
* degrades `chat.abort` to a logged no-op (Stop still seals the UI) rather than
|
|
1197
|
+
* a hard failure.
|
|
1198
|
+
*/
|
|
1199
|
+
function loadOpenClawSdk(req, log, anchor) {
|
|
1200
|
+
let resolvedDispatch = false;
|
|
1201
|
+
try {
|
|
1202
|
+
const channelInbound = req("openclaw/plugin-sdk/channel-inbound");
|
|
1203
|
+
if (channelInbound.dispatchInboundDirectDmWithRuntime) {
|
|
1204
|
+
dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
|
|
1205
|
+
resolvedDispatch = true;
|
|
1206
|
+
}
|
|
1207
|
+
} catch {}
|
|
1208
|
+
try {
|
|
1209
|
+
const harness = req("openclaw/plugin-sdk/agent-harness");
|
|
1210
|
+
if (harness.abortEmbeddedAgentRun) abortEmbeddedAgentRun = harness.abortEmbeddedAgentRun;
|
|
1211
|
+
if (harness.resolveActiveEmbeddedRunSessionId) resolveActiveRunSessionId = harness.resolveActiveEmbeddedRunSessionId;
|
|
1212
|
+
} catch {}
|
|
1213
|
+
try {
|
|
1214
|
+
const envelope = req("openclaw/plugin-sdk/inbound-envelope");
|
|
1215
|
+
if (envelope.resolveInboundRouteEnvelopeBuilderWithRuntime) resolveRouteEnvelope = envelope.resolveInboundRouteEnvelopeBuilderWithRuntime;
|
|
1216
|
+
} catch {}
|
|
1217
|
+
if (resolvedDispatch) log.info(`Resolved OpenClaw SDK from ${anchor} (hard-abort=${abortEmbeddedAgentRun && resolveActiveRunSessionId ? "available" : "unavailable"})`);
|
|
1218
|
+
return resolvedDispatch;
|
|
1219
|
+
}
|
|
1041
1220
|
/**
|
|
1042
1221
|
* Resolve OpenClaw SDK functions from the running process.
|
|
1043
1222
|
*
|
|
@@ -1049,21 +1228,11 @@ let dispatchInbound = null;
|
|
|
1049
1228
|
function resolveOpenClawSdk(log) {
|
|
1050
1229
|
const anchors = [require.main?.filename, process.argv[1]].filter(Boolean);
|
|
1051
1230
|
for (const anchor of anchors) try {
|
|
1052
|
-
|
|
1053
|
-
if (channelInbound.dispatchInboundDirectDmWithRuntime) {
|
|
1054
|
-
dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
|
|
1055
|
-
log.info(`Resolved OpenClaw SDK from ${anchor}`);
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1231
|
+
if (loadOpenClawSdk(createRequire(anchor), log, anchor)) return;
|
|
1058
1232
|
} catch {}
|
|
1059
1233
|
try {
|
|
1060
1234
|
const derivedPath = join(resolve(dirname(process.execPath), ".."), "lib", "node_modules", "openclaw", "package.json");
|
|
1061
|
-
|
|
1062
|
-
if (channelInbound.dispatchInboundDirectDmWithRuntime) {
|
|
1063
|
-
dispatchInbound = channelInbound.dispatchInboundDirectDmWithRuntime;
|
|
1064
|
-
log.info(`Resolved OpenClaw SDK from ${derivedPath}`);
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1235
|
+
if (loadOpenClawSdk(createRequire(derivedPath), log, derivedPath)) return;
|
|
1067
1236
|
} catch {}
|
|
1068
1237
|
log.warn("OpenClaw SDK not resolvable — chat dispatch will not work");
|
|
1069
1238
|
}
|
|
@@ -1140,11 +1309,111 @@ async function downloadAttachments(attachments, log) {
|
|
|
1140
1309
|
}
|
|
1141
1310
|
return results;
|
|
1142
1311
|
}
|
|
1143
|
-
let
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1312
|
+
let agentTurnBacklog = [];
|
|
1313
|
+
let agentTurnDraining = false;
|
|
1314
|
+
function enqueueAgentTurn(task, opts) {
|
|
1315
|
+
return new Promise((resolve, reject) => {
|
|
1316
|
+
const entry = {
|
|
1317
|
+
run: task,
|
|
1318
|
+
coalesceKey: opts?.coalesceKey,
|
|
1319
|
+
onSuperseded: opts?.onSuperseded ?? (() => void 0),
|
|
1320
|
+
resolve,
|
|
1321
|
+
reject
|
|
1322
|
+
};
|
|
1323
|
+
if (entry.coalesceKey) {
|
|
1324
|
+
const survivors = [];
|
|
1325
|
+
for (const queued of agentTurnBacklog) if (queued.coalesceKey === entry.coalesceKey) {
|
|
1326
|
+
try {
|
|
1327
|
+
queued.onSuperseded();
|
|
1328
|
+
} catch {}
|
|
1329
|
+
queued.resolve();
|
|
1330
|
+
} else survivors.push(queued);
|
|
1331
|
+
agentTurnBacklog = survivors;
|
|
1332
|
+
}
|
|
1333
|
+
agentTurnBacklog.push(entry);
|
|
1334
|
+
drainAgentTurns();
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
async function drainAgentTurns() {
|
|
1338
|
+
if (agentTurnDraining) return;
|
|
1339
|
+
agentTurnDraining = true;
|
|
1340
|
+
try {
|
|
1341
|
+
while (agentTurnBacklog.length > 0) {
|
|
1342
|
+
const entry = agentTurnBacklog.shift();
|
|
1343
|
+
if (!entry) break;
|
|
1344
|
+
try {
|
|
1345
|
+
await entry.run();
|
|
1346
|
+
entry.resolve();
|
|
1347
|
+
} catch (err) {
|
|
1348
|
+
entry.reject(err);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
} finally {
|
|
1352
|
+
agentTurnDraining = false;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Test-only: reset the agent-turn backlog between unit tests. Not used at
|
|
1357
|
+
* runtime (the backlog lives for the process lifetime of one daemon).
|
|
1358
|
+
*/
|
|
1359
|
+
function __resetAgentTurnQueueForTest() {
|
|
1360
|
+
agentTurnBacklog = [];
|
|
1361
|
+
agentTurnDraining = false;
|
|
1362
|
+
}
|
|
1363
|
+
/** Test-only accessor for the coalescing queue (see __tests__/queue.test.ts). */
|
|
1364
|
+
const __agentTurnQueueForTest = {
|
|
1365
|
+
enqueue: enqueueAgentTurn,
|
|
1366
|
+
backlogLength: () => agentTurnBacklog.length
|
|
1367
|
+
};
|
|
1368
|
+
let activeRun = null;
|
|
1369
|
+
/** Test-only: set/clear the active-run ref for handleChatAbort unit tests. */
|
|
1370
|
+
function __setActiveRunForTest(ref) {
|
|
1371
|
+
activeRun = ref;
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Ordered session keys to try when aborting for `abortSessionKey`. Pure, so it
|
|
1375
|
+
* unit-tests without the OpenClaw SDK. The resolved route key (what the run
|
|
1376
|
+
* registered under) is preferred; the raw request key is a best-effort
|
|
1377
|
+
* fallback. An abort with NO sessionKey targets the single active run. A
|
|
1378
|
+
* sessionKey that does NOT match the running turn yields no candidates — we
|
|
1379
|
+
* never abort another session's run on a shared daemon.
|
|
1380
|
+
*/
|
|
1381
|
+
function resolveAbortTargetKeys(active, abortSessionKey) {
|
|
1382
|
+
const keys = [];
|
|
1383
|
+
if (active && (!abortSessionKey || active.requestSessionKey === abortSessionKey)) {
|
|
1384
|
+
if (active.routeSessionKey) keys.push(active.routeSessionKey);
|
|
1385
|
+
if (!keys.includes(active.requestSessionKey)) keys.push(active.requestSessionKey);
|
|
1386
|
+
} else if (abortSessionKey) keys.push(abortSessionKey);
|
|
1387
|
+
return keys;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* `chat.abort` handler — genuinely cancels the RUNNING turn on the daemon.
|
|
1391
|
+
*
|
|
1392
|
+
* Invoked OUT-OF-BAND (never through the serialised agent-turn queue) so it
|
|
1393
|
+
* executes immediately while a turn is mid-flight. Resolves the live run's
|
|
1394
|
+
* sessionId from the target session key and fires the run handle's
|
|
1395
|
+
* AbortController via `abortEmbeddedAgentRun`. The in-flight dispatch then
|
|
1396
|
+
* settles (AbortError), so `handleAgentRequest` sends its RPC response and the
|
|
1397
|
+
* chat service seals the UI. When the abort primitives are unavailable (older
|
|
1398
|
+
* openclaw), this degrades to a logged no-op: the UI is still sealed by the
|
|
1399
|
+
* chat service's optimistic idle broadcast, but the turn completes server-side.
|
|
1400
|
+
*/
|
|
1401
|
+
function handleChatAbort(request, log) {
|
|
1402
|
+
const { sessionKey } = request.params;
|
|
1403
|
+
const targetKeys = resolveAbortTargetKeys(activeRun, sessionKey);
|
|
1404
|
+
let aborted = false;
|
|
1405
|
+
if (abortEmbeddedAgentRun && resolveActiveRunSessionId) {
|
|
1406
|
+
for (const key of targetKeys) {
|
|
1407
|
+
const sessionId = resolveActiveRunSessionId(key);
|
|
1408
|
+
if (sessionId && abortEmbeddedAgentRun(sessionId)) {
|
|
1409
|
+
aborted = true;
|
|
1410
|
+
log.info(`chat.abort: hard-aborted running turn (sessionId=${sessionId}, key=${key})`);
|
|
1411
|
+
break;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (!aborted) log.info(`chat.abort: no active run to abort (candidates=${String(targetKeys.length)})`);
|
|
1415
|
+
} else log.warn("chat.abort: OpenClaw abort primitives unavailable — cannot hard-abort the running turn");
|
|
1416
|
+
chatClient?.sendResponse(request.id, true, { aborted });
|
|
1148
1417
|
}
|
|
1149
1418
|
async function handleAgentRequest(request, log) {
|
|
1150
1419
|
const runtime = pluginRuntime;
|
|
@@ -1171,6 +1440,43 @@ async function handleAgentRequest(request, log) {
|
|
|
1171
1440
|
runId: null,
|
|
1172
1441
|
sessionKey: null
|
|
1173
1442
|
};
|
|
1443
|
+
let lastThinkingLen = 0;
|
|
1444
|
+
const turnThinking = {
|
|
1445
|
+
text: "",
|
|
1446
|
+
ts: 0
|
|
1447
|
+
};
|
|
1448
|
+
const turnTools = /* @__PURE__ */ new Map();
|
|
1449
|
+
const recordThinking = (text, ts) => {
|
|
1450
|
+
if (!turnThinking.text) turnThinking.ts = ts;
|
|
1451
|
+
turnThinking.text += text;
|
|
1452
|
+
};
|
|
1453
|
+
const recordTool = (activity, ts) => {
|
|
1454
|
+
const prev = turnTools.get(activity.toolCallId);
|
|
1455
|
+
turnTools.set(activity.toolCallId, {
|
|
1456
|
+
kind: "tool",
|
|
1457
|
+
toolCallId: activity.toolCallId,
|
|
1458
|
+
name: activity.name,
|
|
1459
|
+
status: activity.status === "running" ? prev?.status ?? "interrupted" : activity.status,
|
|
1460
|
+
...activity.summary ?? prev?.summary ? { summary: activity.summary ?? prev?.summary } : {},
|
|
1461
|
+
...prev?.argsText ?? activity.argsText ? { argsText: prev?.argsText ?? activity.argsText } : {},
|
|
1462
|
+
...activity.resultText ?? prev?.resultText ? { resultText: activity.resultText ?? prev?.resultText } : {},
|
|
1463
|
+
...(activity.isError ?? prev?.isError) !== void 0 ? { isError: activity.isError ?? prev?.isError } : {},
|
|
1464
|
+
...(activity.durationMs ?? prev?.durationMs) !== void 0 ? { durationMs: activity.durationMs ?? prev?.durationMs } : {},
|
|
1465
|
+
...activity.truncated ?? prev?.truncated ? { truncated: activity.truncated ?? prev?.truncated } : {},
|
|
1466
|
+
ts: prev?.ts ?? ts
|
|
1467
|
+
});
|
|
1468
|
+
};
|
|
1469
|
+
const flushTurnActivity = async () => {
|
|
1470
|
+
const entries = [];
|
|
1471
|
+
if (turnThinking.text) entries.push({
|
|
1472
|
+
kind: "thinking",
|
|
1473
|
+
text: turnThinking.text,
|
|
1474
|
+
ts: turnThinking.ts
|
|
1475
|
+
});
|
|
1476
|
+
for (const tool of turnTools.values()) entries.push(tool);
|
|
1477
|
+
entries.sort((a, b) => a.ts - b.ts);
|
|
1478
|
+
await appendActivity(sessionId, entries);
|
|
1479
|
+
};
|
|
1174
1480
|
const UPDATE_THROTTLE_MS = 250;
|
|
1175
1481
|
const pendingUpdates = /* @__PURE__ */ new Map();
|
|
1176
1482
|
const flushToolUpdate = (toolCallId) => {
|
|
@@ -1187,33 +1493,58 @@ async function handleAgentRequest(request, log) {
|
|
|
1187
1493
|
const unsubscribe = runtime.events.onAgentEvent((evt) => {
|
|
1188
1494
|
if (!admitAgentEvent(runGate, evt)) return;
|
|
1189
1495
|
if (evt.stream === "assistant") {
|
|
1496
|
+
const raw = joinAssistantText(evt.data);
|
|
1497
|
+
const { visible, thinking } = splitThinkTags(raw);
|
|
1498
|
+
if (thinking.length > lastThinkingLen) {
|
|
1499
|
+
const thinkingDelta = thinking.slice(lastThinkingLen);
|
|
1500
|
+
chatClient?.sendEvent("chat-activity", {
|
|
1501
|
+
runId: evt.runId,
|
|
1502
|
+
sessionKey: legacySessionKey,
|
|
1503
|
+
seq: evt.seq,
|
|
1504
|
+
state: "activity",
|
|
1505
|
+
activity: {
|
|
1506
|
+
kind: "thinking",
|
|
1507
|
+
text: thinkingDelta,
|
|
1508
|
+
delta: true,
|
|
1509
|
+
ts: evt.ts,
|
|
1510
|
+
seq: evt.seq
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
recordThinking(thinkingDelta, evt.ts);
|
|
1514
|
+
lastThinkingLen = thinking.length;
|
|
1515
|
+
}
|
|
1516
|
+
const message = visible === raw && thinking.length === 0 ? evt.data : rewriteAssistantText(evt.data, visible);
|
|
1190
1517
|
chatClient?.sendEvent("chat", {
|
|
1191
1518
|
runId: evt.runId,
|
|
1192
1519
|
sessionKey: legacySessionKey,
|
|
1193
1520
|
seq: evt.seq,
|
|
1194
1521
|
state: "delta",
|
|
1195
|
-
message
|
|
1522
|
+
message
|
|
1196
1523
|
});
|
|
1197
1524
|
return;
|
|
1198
1525
|
}
|
|
1199
1526
|
if (evt.stream === "thinking") {
|
|
1200
1527
|
const activity = buildThinkingActivity(evt.data);
|
|
1201
|
-
if (activity)
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1528
|
+
if (activity) {
|
|
1529
|
+
chatClient?.sendEvent("chat-activity", {
|
|
1530
|
+
runId: evt.runId,
|
|
1531
|
+
sessionKey: legacySessionKey,
|
|
1532
|
+
seq: evt.seq,
|
|
1533
|
+
state: "activity",
|
|
1534
|
+
activity: {
|
|
1535
|
+
...activity,
|
|
1536
|
+
ts: evt.ts,
|
|
1537
|
+
seq: evt.seq
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
if (activity.kind === "thinking") recordThinking(activity.text, evt.ts);
|
|
1541
|
+
}
|
|
1212
1542
|
return;
|
|
1213
1543
|
}
|
|
1214
1544
|
if (evt.stream === "tool") {
|
|
1215
1545
|
const activity = buildToolActivity(evt.data);
|
|
1216
1546
|
if (activity?.kind !== "tool") return;
|
|
1547
|
+
recordTool(activity, evt.ts);
|
|
1217
1548
|
const toolCallId = activity.toolCallId;
|
|
1218
1549
|
const send = () => {
|
|
1219
1550
|
chatClient?.sendEvent("chat-activity", {
|
|
@@ -1263,19 +1594,37 @@ async function handleAgentRequest(request, log) {
|
|
|
1263
1594
|
const conversationLabel = conversationType === "group" ? shortConvId ? `[${channelLabel}] Group (${shortConvId})` : `[${channelLabel}] Group` : shortConvId ? `[${channelLabel}] ${userLabel} (${shortConvId})` : `[${channelLabel}] ${userLabel}`;
|
|
1264
1595
|
let a2aResponseBuffer = "";
|
|
1265
1596
|
if (isA2A) resetA2AEndSignal();
|
|
1597
|
+
const peer = conversationType === "group" ? {
|
|
1598
|
+
kind: "group",
|
|
1599
|
+
id: conversationId ?? senderId
|
|
1600
|
+
} : {
|
|
1601
|
+
kind: "direct",
|
|
1602
|
+
id: conversationId ? `${senderId}:conv:${conversationId}` : senderId
|
|
1603
|
+
};
|
|
1604
|
+
let routeSessionKey = null;
|
|
1605
|
+
if (resolveRouteEnvelope) try {
|
|
1606
|
+
routeSessionKey = resolveRouteEnvelope({
|
|
1607
|
+
cfg,
|
|
1608
|
+
channel: "alfe",
|
|
1609
|
+
accountId: "default",
|
|
1610
|
+
peer,
|
|
1611
|
+
runtime: runtime.channel,
|
|
1612
|
+
sessionStore: cfg.session?.store
|
|
1613
|
+
}).route.sessionKey;
|
|
1614
|
+
} catch (err) {
|
|
1615
|
+
log.debug(`Route precompute failed (abort will use raw key): ${err instanceof Error ? err.message : String(err)}`);
|
|
1616
|
+
}
|
|
1617
|
+
activeRun = {
|
|
1618
|
+
requestSessionKey: legacySessionKey,
|
|
1619
|
+
routeSessionKey
|
|
1620
|
+
};
|
|
1266
1621
|
runGate.sessionKey = (await dispatchInbound({
|
|
1267
1622
|
cfg,
|
|
1268
1623
|
runtime: { channel: runtime.channel },
|
|
1269
1624
|
channel: "alfe",
|
|
1270
1625
|
channelLabel,
|
|
1271
1626
|
accountId: "default",
|
|
1272
|
-
peer
|
|
1273
|
-
kind: "group",
|
|
1274
|
-
id: conversationId ?? senderId
|
|
1275
|
-
} : {
|
|
1276
|
-
kind: "direct",
|
|
1277
|
-
id: conversationId ? `${senderId}:conv:${conversationId}` : senderId
|
|
1278
|
-
},
|
|
1627
|
+
peer,
|
|
1279
1628
|
senderId,
|
|
1280
1629
|
senderAddress: senderId,
|
|
1281
1630
|
recipientAddress: "agent",
|
|
@@ -1309,10 +1658,10 @@ async function handleAgentRequest(request, log) {
|
|
|
1309
1658
|
} : {}
|
|
1310
1659
|
},
|
|
1311
1660
|
deliver: async (payload) => {
|
|
1312
|
-
const responseText = stripLeakedToolSummary(payload.text ?? "");
|
|
1661
|
+
const responseText = stripLeakedToolSummary(stripThinkSpans(payload.text ?? ""));
|
|
1313
1662
|
const mediaUrls = [...payload.mediaUrl ? [payload.mediaUrl] : [], ...payload.mediaUrls ?? []];
|
|
1314
1663
|
await addMessage(sessionId, "assistant", responseText);
|
|
1315
|
-
if (isA2A) a2aResponseBuffer += responseText;
|
|
1664
|
+
if (isA2A) a2aResponseBuffer += (a2aResponseBuffer && responseText ? "\n\n" : "") + responseText;
|
|
1316
1665
|
chatClient?.notify("agent-message", {
|
|
1317
1666
|
conversationId: conversationId ?? legacySessionKey,
|
|
1318
1667
|
text: responseText,
|
|
@@ -1344,8 +1693,14 @@ async function handleAgentRequest(request, log) {
|
|
|
1344
1693
|
log.error(`Agent dispatch failed: ${errMsg}`);
|
|
1345
1694
|
chatClient?.sendResponse(request.id, false, { message: errMsg });
|
|
1346
1695
|
} finally {
|
|
1696
|
+
activeRun = null;
|
|
1347
1697
|
unsubscribe();
|
|
1348
1698
|
clearAllToolUpdates();
|
|
1699
|
+
try {
|
|
1700
|
+
await flushTurnActivity();
|
|
1701
|
+
} catch (err) {
|
|
1702
|
+
log.error(`Activity persistence failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1703
|
+
}
|
|
1349
1704
|
if (isA2A) disarmA2AEndSignal();
|
|
1350
1705
|
}
|
|
1351
1706
|
}
|
|
@@ -1371,14 +1726,16 @@ async function handleSessionsGet(request, log) {
|
|
|
1371
1726
|
if (!session) {
|
|
1372
1727
|
chatClient?.sendResponse(request.id, true, {
|
|
1373
1728
|
ok: false,
|
|
1374
|
-
error: "Session not found"
|
|
1729
|
+
error: "Session not found",
|
|
1730
|
+
messages: []
|
|
1375
1731
|
});
|
|
1376
1732
|
return;
|
|
1377
1733
|
}
|
|
1378
1734
|
if (params.userId && session.userId && session.userId !== params.userId) {
|
|
1379
1735
|
chatClient?.sendResponse(request.id, true, {
|
|
1380
1736
|
ok: false,
|
|
1381
|
-
error: "Session not found"
|
|
1737
|
+
error: "Session not found",
|
|
1738
|
+
messages: []
|
|
1382
1739
|
});
|
|
1383
1740
|
return;
|
|
1384
1741
|
}
|
|
@@ -1392,7 +1749,8 @@ async function handleSessionsGet(request, log) {
|
|
|
1392
1749
|
role: m.role,
|
|
1393
1750
|
content: m.content,
|
|
1394
1751
|
timestamp: m.timestamp
|
|
1395
|
-
}))
|
|
1752
|
+
})),
|
|
1753
|
+
activity: (session.activity ?? []).map((a) => ({ ...a }))
|
|
1396
1754
|
});
|
|
1397
1755
|
} catch (err) {
|
|
1398
1756
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -1444,7 +1802,20 @@ const plugin = {
|
|
|
1444
1802
|
apiKey,
|
|
1445
1803
|
onRequest: (request) => {
|
|
1446
1804
|
const handle = async () => {
|
|
1447
|
-
if (request.method === "agent")
|
|
1805
|
+
if (request.method === "agent") {
|
|
1806
|
+
const params = request.params;
|
|
1807
|
+
const coalesceKey = params.coalesce === "replace" && typeof params.sessionKey === "string" ? params.sessionKey : void 0;
|
|
1808
|
+
await enqueueAgentTurn(() => handleAgentRequest(request, log), coalesceKey ? {
|
|
1809
|
+
coalesceKey,
|
|
1810
|
+
onSuperseded: () => {
|
|
1811
|
+
log.info(`Agent turn superseded before start (coalesce=replace): ${coalesceKey}`);
|
|
1812
|
+
chatClient?.sendResponse(request.id, true, {
|
|
1813
|
+
superseded: true,
|
|
1814
|
+
sessionKey: coalesceKey
|
|
1815
|
+
});
|
|
1816
|
+
}
|
|
1817
|
+
} : void 0);
|
|
1818
|
+
} else if (request.method === "chat.abort") handleChatAbort(request, log);
|
|
1448
1819
|
else if (request.method === "sessions.list") await handleSessionsList(request, log);
|
|
1449
1820
|
else if (request.method === "sessions.get") await handleSessionsGet(request, log);
|
|
1450
1821
|
else chatClient?.sendResponse(request.id, false, { message: `Unknown method: ${request.method}` });
|
|
@@ -1500,11 +1871,13 @@ const plugin = {
|
|
|
1500
1871
|
const session = await getSession(params.sessionId);
|
|
1501
1872
|
if (!session) return {
|
|
1502
1873
|
ok: false,
|
|
1503
|
-
error: "Session not found"
|
|
1874
|
+
error: "Session not found",
|
|
1875
|
+
messages: []
|
|
1504
1876
|
};
|
|
1505
1877
|
if (params.userId && session.userId && session.userId !== params.userId) return {
|
|
1506
1878
|
ok: false,
|
|
1507
|
-
error: "Session not found"
|
|
1879
|
+
error: "Session not found",
|
|
1880
|
+
messages: []
|
|
1508
1881
|
};
|
|
1509
1882
|
return {
|
|
1510
1883
|
sessionId: session.sessionId,
|
|
@@ -1516,7 +1889,8 @@ const plugin = {
|
|
|
1516
1889
|
role: m.role,
|
|
1517
1890
|
content: m.content,
|
|
1518
1891
|
timestamp: m.timestamp
|
|
1519
|
-
}))
|
|
1892
|
+
})),
|
|
1893
|
+
activity: (session.activity ?? []).map((a) => ({ ...a }))
|
|
1520
1894
|
};
|
|
1521
1895
|
});
|
|
1522
1896
|
gw.__alfeChatGatewayMethodsRegistered = true;
|
|
@@ -1574,4 +1948,4 @@ const plugin = {
|
|
|
1574
1948
|
}
|
|
1575
1949
|
};
|
|
1576
1950
|
//#endregion
|
|
1577
|
-
export {
|
|
1951
|
+
export { buildA2ACompletePayload as a, plugin as c, stripLeakedToolSummary as d, createAlfeChannelPlugin as f, admitAgentEvent as i, resolveAbortTargetKeys as l, __resetAgentTurnQueueForTest as n, buildToolActivity as o, __setActiveRunForTest as r, joinAssistantText as s, __agentTurnQueueForTest as t, rewriteAssistantText as u };
|