@alfe.ai/openclaw-chat 0.2.5 → 0.3.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 +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.cjs +1 -0
- package/dist/plugin.d.cts +35 -1
- package/dist/plugin.d.ts +35 -1
- package/dist/plugin.js +2 -2
- package/dist/plugin2.cjs +323 -12
- package/dist/plugin2.d.cts +2 -2
- package/dist/plugin2.d.ts +2 -2
- package/dist/plugin2.js +318 -13
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as createAlfeChannelPlugin, c as AlfeResolvedAccount, o as AlfeChannelConfig, r as plugin, s as AlfePluginConfig } 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 createAlfeChannelPlugin, c as AlfeResolvedAccount, o as AlfeChannelConfig, r as plugin, s as AlfePluginConfig } 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 { a as createAlfeChannelPlugin, r as plugin } from "./plugin2.js";
|
|
2
2
|
export { createAlfeChannelPlugin, plugin as default };
|
package/dist/plugin.cjs
CHANGED
|
@@ -4,5 +4,6 @@ Object.defineProperties(exports, {
|
|
|
4
4
|
});
|
|
5
5
|
const require_plugin = require("./plugin2.cjs");
|
|
6
6
|
exports.buildA2ACompletePayload = require_plugin.buildA2ACompletePayload;
|
|
7
|
+
exports.buildToolActivity = require_plugin.buildToolActivity;
|
|
7
8
|
exports.default = require_plugin.plugin;
|
|
8
9
|
exports.stripLeakedToolSummary = require_plugin.stripLeakedToolSummary;
|
package/dist/plugin.d.cts
CHANGED
|
@@ -274,6 +274,40 @@ interface PluginServiceContext {
|
|
|
274
274
|
stateDir: string;
|
|
275
275
|
logger: PluginLogger;
|
|
276
276
|
}
|
|
277
|
+
type ChatActivity = {
|
|
278
|
+
kind: 'thinking';
|
|
279
|
+
text: string;
|
|
280
|
+
delta?: boolean;
|
|
281
|
+
ts?: number;
|
|
282
|
+
seq?: number;
|
|
283
|
+
} | {
|
|
284
|
+
kind: 'tool';
|
|
285
|
+
toolCallId: string;
|
|
286
|
+
name: string;
|
|
287
|
+
phase?: string;
|
|
288
|
+
status: 'running' | 'done' | 'failed';
|
|
289
|
+
summary?: string;
|
|
290
|
+
argsText?: string;
|
|
291
|
+
progressText?: string;
|
|
292
|
+
resultText?: string;
|
|
293
|
+
isError?: boolean;
|
|
294
|
+
durationMs?: number;
|
|
295
|
+
truncated?: {
|
|
296
|
+
args?: boolean;
|
|
297
|
+
progress?: boolean;
|
|
298
|
+
result?: boolean;
|
|
299
|
+
};
|
|
300
|
+
ts?: number;
|
|
301
|
+
seq?: number;
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* Extract structured tool status from an OpenClaw `tool`-stream event.
|
|
305
|
+
* We send the emoji-free raw `name` plus a lifecycle `status` and let the
|
|
306
|
+
* web render the `🛠️ <Label>: <summary> ✓/✗` row — never OpenClaw's raw
|
|
307
|
+
* `(+N steps)` string. `summary` carries OpenClaw's short detail when the
|
|
308
|
+
* event already provides one (title/summary/label/description), trimmed.
|
|
309
|
+
*/
|
|
310
|
+
declare function buildToolActivity(data: Record<string, unknown>): ChatActivity | null;
|
|
277
311
|
declare function stripLeakedToolSummary(text: string): string;
|
|
278
312
|
/**
|
|
279
313
|
* Build the `a2a-complete` notify payload. ALWAYS fired for an A2A turn —
|
|
@@ -328,4 +362,4 @@ declare const plugin: {
|
|
|
328
362
|
deactivate(api: PluginApi): Promise<void>;
|
|
329
363
|
};
|
|
330
364
|
//#endregion
|
|
331
|
-
export {
|
|
365
|
+
export { createAlfeChannelPlugin as a, AlfeResolvedAccount as c, stripLeakedToolSummary as i, buildToolActivity as n, AlfeChannelConfig as o, plugin as r, AlfePluginConfig as s, buildA2ACompletePayload as t };
|
package/dist/plugin.d.ts
CHANGED
|
@@ -274,6 +274,40 @@ interface PluginServiceContext {
|
|
|
274
274
|
stateDir: string;
|
|
275
275
|
logger: PluginLogger;
|
|
276
276
|
}
|
|
277
|
+
type ChatActivity = {
|
|
278
|
+
kind: 'thinking';
|
|
279
|
+
text: string;
|
|
280
|
+
delta?: boolean;
|
|
281
|
+
ts?: number;
|
|
282
|
+
seq?: number;
|
|
283
|
+
} | {
|
|
284
|
+
kind: 'tool';
|
|
285
|
+
toolCallId: string;
|
|
286
|
+
name: string;
|
|
287
|
+
phase?: string;
|
|
288
|
+
status: 'running' | 'done' | 'failed';
|
|
289
|
+
summary?: string;
|
|
290
|
+
argsText?: string;
|
|
291
|
+
progressText?: string;
|
|
292
|
+
resultText?: string;
|
|
293
|
+
isError?: boolean;
|
|
294
|
+
durationMs?: number;
|
|
295
|
+
truncated?: {
|
|
296
|
+
args?: boolean;
|
|
297
|
+
progress?: boolean;
|
|
298
|
+
result?: boolean;
|
|
299
|
+
};
|
|
300
|
+
ts?: number;
|
|
301
|
+
seq?: number;
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* Extract structured tool status from an OpenClaw `tool`-stream event.
|
|
305
|
+
* We send the emoji-free raw `name` plus a lifecycle `status` and let the
|
|
306
|
+
* web render the `🛠️ <Label>: <summary> ✓/✗` row — never OpenClaw's raw
|
|
307
|
+
* `(+N steps)` string. `summary` carries OpenClaw's short detail when the
|
|
308
|
+
* event already provides one (title/summary/label/description), trimmed.
|
|
309
|
+
*/
|
|
310
|
+
declare function buildToolActivity(data: Record<string, unknown>): ChatActivity | null;
|
|
277
311
|
declare function stripLeakedToolSummary(text: string): string;
|
|
278
312
|
/**
|
|
279
313
|
* Build the `a2a-complete` notify payload. ALWAYS fired for an A2A turn —
|
|
@@ -328,4 +362,4 @@ declare const plugin: {
|
|
|
328
362
|
deactivate(api: PluginApi): Promise<void>;
|
|
329
363
|
};
|
|
330
364
|
//#endregion
|
|
331
|
-
export {
|
|
365
|
+
export { createAlfeChannelPlugin as a, AlfeResolvedAccount as c, stripLeakedToolSummary as i, buildToolActivity as n, AlfeChannelConfig as o, plugin as r, AlfePluginConfig as s, buildA2ACompletePayload as t };
|
package/dist/plugin.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
export { buildA2ACompletePayload, plugin as default, stripLeakedToolSummary };
|
|
1
|
+
import { i as stripLeakedToolSummary, n as buildToolActivity, r as plugin, t as buildA2ACompletePayload } from "./plugin2.js";
|
|
2
|
+
export { buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
|
package/dist/plugin2.cjs
CHANGED
|
@@ -619,6 +619,235 @@ function validateAttachmentUrl(input, opts = {}) {
|
|
|
619
619
|
};
|
|
620
620
|
}
|
|
621
621
|
//#endregion
|
|
622
|
+
//#region src/activity-serialize.ts
|
|
623
|
+
/**
|
|
624
|
+
* Console-grade tool-activity serialization (Slice 3).
|
|
625
|
+
*
|
|
626
|
+
* The chat plugin forwards a curated activity stream to the chat service.
|
|
627
|
+
* For tool calls we now carry the args / streaming progress / final result
|
|
628
|
+
* so the client can render an OpenClaw-console-grade card. Those payloads
|
|
629
|
+
* arrive from OpenClaw's `onAgentEvent` bus as arbitrary `unknown`-shaped
|
|
630
|
+
* objects (`args` is the raw tool-call arguments; `partialResult` / `result`
|
|
631
|
+
* are tool-result envelopes). We must render them to BOUNDED strings BEFORE
|
|
632
|
+
* they cross the WS to the chat service, because:
|
|
633
|
+
*
|
|
634
|
+
* 1. Daemon WS frames are `maxPayload`-bounded — an uncapped multi-MB tool
|
|
635
|
+
* stdout would drop the WHOLE frame (losing the status transition too),
|
|
636
|
+
* so the cap has to live on the emit side, not the client.
|
|
637
|
+
* 2. The relay is a dumb passthrough and the reducer is pure — neither
|
|
638
|
+
* should have to serialize or sanitize unknown objects. The wire carries
|
|
639
|
+
* display text, never raw objects.
|
|
640
|
+
*
|
|
641
|
+
* OpenClaw's own `sanitizeToolResult` already strips standard image content
|
|
642
|
+
* blocks (`{type:'image', data}` → `{omitted:true}`) and truncates text on
|
|
643
|
+
* the `result`/`partialResult` path — but (a) its helpers are NOT importable
|
|
644
|
+
* from the plugin (openclaw is a runtime peer dep, absent from node_modules),
|
|
645
|
+
* and (b) `args` is NOT sanitized at all and can carry base64 (an inline
|
|
646
|
+
* image arg, a `data:` URI in a write). So we reimplement a defensive
|
|
647
|
+
* strip+cap here and apply it to every field, regardless of upstream
|
|
648
|
+
* sanitization.
|
|
649
|
+
*/
|
|
650
|
+
/** Args render cap — args are usually small; 4KB is generous. */
|
|
651
|
+
const ARGS_CAP = 4 * 1024;
|
|
652
|
+
/** Streaming progress window — keep the newest 4KB (tail-truncated). */
|
|
653
|
+
const PROGRESS_CAP = 4 * 1024;
|
|
654
|
+
/** Final result cap — results can be large (file reads, search); 16KB. */
|
|
655
|
+
const RESULT_CAP = 16 * 1024;
|
|
656
|
+
/**
|
|
657
|
+
* Replace long base64 runs, `data:` URIs, and known image content-block
|
|
658
|
+
* shapes with a placeholder. Runs BEFORE JSON serialization on a deep-cloned
|
|
659
|
+
* structure so we never mutate OpenClaw's live objects.
|
|
660
|
+
*
|
|
661
|
+
* Detection heuristics (all conservative — we only replace clear payloads):
|
|
662
|
+
* - A string that is a `data:` URI → `[data-uri omitted]`.
|
|
663
|
+
* - A string that is a long (≥256 char) run of base64 chars → `[base64 omitted]`.
|
|
664
|
+
* 256 is well above any realistic id/token/hash and below inline media.
|
|
665
|
+
* - An object that looks like an image content block (`type:'image'` with a
|
|
666
|
+
* `data`/`source`/`url` field, OR an Anthropic `{source:{data}}` shape) →
|
|
667
|
+
* the payload field is replaced with `[image omitted]`.
|
|
668
|
+
*/
|
|
669
|
+
const DATA_URI_RE = /^data:[^;,]*(;[^,]*)?,/i;
|
|
670
|
+
const BASE64_BLOB_RE = /^[A-Za-z0-9+/=\r\n]+$/;
|
|
671
|
+
const BASE64_MIN_LEN = 256;
|
|
672
|
+
function looksLikeBase64Blob(s) {
|
|
673
|
+
if (s.length < BASE64_MIN_LEN) return false;
|
|
674
|
+
return s.replace(/[\r\n]/g, "").length >= BASE64_MIN_LEN && BASE64_BLOB_RE.test(s);
|
|
675
|
+
}
|
|
676
|
+
const EMBEDDED_DATA_URI_RE = /data:[^;,\s"'`]{0,64}(?:;[^,\s"'`]{0,32})?,[A-Za-z0-9+/=]{128,}/g;
|
|
677
|
+
const EMBEDDED_BASE64_RE = /[A-Za-z0-9+/]{512,}={0,2}/g;
|
|
678
|
+
/**
|
|
679
|
+
* Post-match filter for EMBEDDED_BASE64_RE: real base64 media is
|
|
680
|
+
* high-entropy — a 512+ char run virtually always mixes lower, upper, and
|
|
681
|
+
* digits. This keeps legitimate long runs (a repeated fill char in test
|
|
682
|
+
* output, an ASCII divider) from being eaten by the strip.
|
|
683
|
+
*/
|
|
684
|
+
function looksHighEntropyBase64(run) {
|
|
685
|
+
return /[a-z]/.test(run) && /[A-Z]/.test(run) && /\d/.test(run);
|
|
686
|
+
}
|
|
687
|
+
function stripStringMedia(s) {
|
|
688
|
+
if (DATA_URI_RE.test(s)) return "[data-uri omitted]";
|
|
689
|
+
if (looksLikeBase64Blob(s)) return "[base64 omitted]";
|
|
690
|
+
if (s.length >= 128) return s.replace(EMBEDDED_DATA_URI_RE, "[data-uri omitted]").replace(EMBEDDED_BASE64_RE, (m) => looksHighEntropyBase64(m) ? "[base64 omitted]" : m);
|
|
691
|
+
return s;
|
|
692
|
+
}
|
|
693
|
+
const MEDIA_PAYLOAD_KEYS = new Set([
|
|
694
|
+
"data",
|
|
695
|
+
"base64",
|
|
696
|
+
"b64_json",
|
|
697
|
+
"bytes"
|
|
698
|
+
]);
|
|
699
|
+
function isImageBlock(obj) {
|
|
700
|
+
const type = typeof obj.type === "string" ? obj.type.toLowerCase() : "";
|
|
701
|
+
if (type === "image" || type === "image_url" || type === "input_image") return true;
|
|
702
|
+
if (obj.source && typeof obj.source === "object") {
|
|
703
|
+
const src = obj.source;
|
|
704
|
+
if (typeof src.data === "string" || typeof src.url === "string") return true;
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Deep-clone `value` while stripping media. Guards against cycles (returns
|
|
710
|
+
* `'[circular]'`) and caps recursion depth. Non-plain values (functions,
|
|
711
|
+
* symbols) are dropped by JSON semantics later; here we normalize them so the
|
|
712
|
+
* safe-JSON pass can't throw.
|
|
713
|
+
*/
|
|
714
|
+
function stripMediaDeep(value, seen, depth) {
|
|
715
|
+
if (depth > 12) return "[max-depth]";
|
|
716
|
+
if (value === null) return null;
|
|
717
|
+
const t = typeof value;
|
|
718
|
+
if (t === "string") return stripStringMedia(value);
|
|
719
|
+
if (t === "number" || t === "boolean") return value;
|
|
720
|
+
if (t === "bigint") return `${value.toString()}n`;
|
|
721
|
+
if (t === "undefined" || t === "function" || t === "symbol") return void 0;
|
|
722
|
+
const obj = value;
|
|
723
|
+
if (seen.has(obj)) return "[circular]";
|
|
724
|
+
seen.add(obj);
|
|
725
|
+
try {
|
|
726
|
+
if (Array.isArray(obj)) return obj.map((item) => stripMediaDeep(item, seen, depth + 1));
|
|
727
|
+
const record = obj;
|
|
728
|
+
const imageBlock = isImageBlock(record);
|
|
729
|
+
const out = {};
|
|
730
|
+
for (const [key, v] of Object.entries(record)) {
|
|
731
|
+
if (imageBlock && MEDIA_PAYLOAD_KEYS.has(key)) {
|
|
732
|
+
out[key] = "[image omitted]";
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (imageBlock && key === "source" && v && typeof v === "object") {
|
|
736
|
+
const src = { ...v };
|
|
737
|
+
for (const mk of MEDIA_PAYLOAD_KEYS) if (typeof src[mk] === "string") src[mk] = "[image omitted]";
|
|
738
|
+
out[key] = src;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
out[key] = stripMediaDeep(v, seen, depth + 1);
|
|
742
|
+
}
|
|
743
|
+
return out;
|
|
744
|
+
} finally {
|
|
745
|
+
seen.delete(obj);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Cycle- and BigInt-safe JSON stringify. Returns `undefined` when there's
|
|
750
|
+
* nothing meaningful to render (null/undefined). A plain string value is
|
|
751
|
+
* returned as-is (unquoted) — args like `"ls -la"` read better raw than
|
|
752
|
+
* `"\"ls -la\""`.
|
|
753
|
+
*/
|
|
754
|
+
function safeJson(value) {
|
|
755
|
+
if (value === null || value === void 0) return void 0;
|
|
756
|
+
const stripped = stripMediaDeep(value, /* @__PURE__ */ new WeakSet(), 0);
|
|
757
|
+
if (stripped === void 0) return void 0;
|
|
758
|
+
if (typeof stripped === "string") return stripped;
|
|
759
|
+
try {
|
|
760
|
+
return JSON.stringify(stripped, null, 2);
|
|
761
|
+
} catch {
|
|
762
|
+
return "[unserializable]";
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/** Head-truncate: keep the START of the text (for args/results). */
|
|
766
|
+
function capHead(text, cap) {
|
|
767
|
+
if (text.length <= cap) return {
|
|
768
|
+
text,
|
|
769
|
+
truncated: false
|
|
770
|
+
};
|
|
771
|
+
return {
|
|
772
|
+
text: `${text.slice(0, cap)}\n… [truncated ${String(text.length - cap)} chars]`,
|
|
773
|
+
truncated: true
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/** Tail-truncate: keep the END of the text (for streaming progress). */
|
|
777
|
+
function capTail(text, cap) {
|
|
778
|
+
if (text.length <= cap) return {
|
|
779
|
+
text,
|
|
780
|
+
truncated: false
|
|
781
|
+
};
|
|
782
|
+
return {
|
|
783
|
+
text: `… [truncated ${String(text.length - cap)} chars]\n${text.slice(text.length - cap)}`,
|
|
784
|
+
truncated: true
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Serialize + strip + head-cap the tool-call `args` (from `phase:start`).
|
|
789
|
+
* Returns `undefined` when there's nothing to show.
|
|
790
|
+
*/
|
|
791
|
+
function serializeArgs(args) {
|
|
792
|
+
const rendered = safeJson(args);
|
|
793
|
+
if (rendered === void 0 || rendered === "" || rendered === "{}") return void 0;
|
|
794
|
+
return capHead(rendered, ARGS_CAP);
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Serialize + strip + head-cap the final tool `result` (from `phase:result`).
|
|
798
|
+
* Prefers rendering the result's text content when it's a standard
|
|
799
|
+
* `{content:[{type:'text',text}]}` envelope (reads far better than the raw
|
|
800
|
+
* JSON), falling back to the full JSON otherwise.
|
|
801
|
+
*/
|
|
802
|
+
function serializeResult(result) {
|
|
803
|
+
if (isEmptyEnvelope(result)) return void 0;
|
|
804
|
+
const rendered = extractResultText(result) ?? safeJson(result);
|
|
805
|
+
if (rendered === void 0 || rendered === "" || rendered === "{}") return void 0;
|
|
806
|
+
return capHead(stripStringMedia(rendered), RESULT_CAP);
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Serialize + strip + TAIL-cap a streaming `partialResult` (from
|
|
810
|
+
* `phase:update`). Tail-truncation keeps the NEWEST output — for a bash/exec
|
|
811
|
+
* stream the tail is what the user cares about. Whether OpenClaw's
|
|
812
|
+
* `partialResult` is cumulative or incremental is version-dependent; treating
|
|
813
|
+
* it as "the latest window" (overwrite, not append — see the reducer) is
|
|
814
|
+
* correct for both: a cumulative stream's newest frame already contains the
|
|
815
|
+
* full window (tail-capped), and an incremental stream's newest frame is the
|
|
816
|
+
* latest chunk.
|
|
817
|
+
*/
|
|
818
|
+
function serializeProgress(partial) {
|
|
819
|
+
if (isEmptyEnvelope(partial)) return void 0;
|
|
820
|
+
const rendered = extractResultText(partial) ?? safeJson(partial);
|
|
821
|
+
if (rendered === void 0 || rendered === "" || rendered === "{}") return void 0;
|
|
822
|
+
return capTail(stripStringMedia(rendered), PROGRESS_CAP);
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Pull the concatenated `text` blocks out of a standard tool-result envelope
|
|
826
|
+
* (`{content:[{type:'text',text}, {type:'image',omitted:true}]}`). Returns
|
|
827
|
+
* `undefined` when the shape isn't a text-bearing envelope, so callers fall
|
|
828
|
+
* back to full-JSON rendering.
|
|
829
|
+
*/
|
|
830
|
+
/** True for a tool-result envelope with an empty `content` array. */
|
|
831
|
+
function isEmptyEnvelope(value) {
|
|
832
|
+
if (!value || typeof value !== "object") return false;
|
|
833
|
+
const content = value.content;
|
|
834
|
+
return Array.isArray(content) && content.length === 0;
|
|
835
|
+
}
|
|
836
|
+
function extractResultText(result) {
|
|
837
|
+
if (!result || typeof result !== "object") return void 0;
|
|
838
|
+
const content = result.content;
|
|
839
|
+
if (!Array.isArray(content)) return void 0;
|
|
840
|
+
const parts = [];
|
|
841
|
+
for (const item of content) {
|
|
842
|
+
if (!item || typeof item !== "object") continue;
|
|
843
|
+
const entry = item;
|
|
844
|
+
if (entry.type === "text" && typeof entry.text === "string") parts.push(entry.text);
|
|
845
|
+
else if (entry.type === "image" || entry.omitted === true) parts.push("[image omitted]");
|
|
846
|
+
}
|
|
847
|
+
if (parts.length === 0) return void 0;
|
|
848
|
+
return parts.join("\n");
|
|
849
|
+
}
|
|
850
|
+
//#endregion
|
|
622
851
|
//#region src/plugin.ts
|
|
623
852
|
/**
|
|
624
853
|
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
@@ -668,17 +897,58 @@ function buildToolActivity(data) {
|
|
|
668
897
|
const rawStatus = (phase ?? "").toLowerCase();
|
|
669
898
|
let status;
|
|
670
899
|
if (rawStatus === "error" || rawStatus === "failed" || data.error != null || data.isError === true) status = "failed";
|
|
671
|
-
else if (rawStatus === "end" || rawStatus === "done" || rawStatus === "success" || rawStatus === "complete") status = "done";
|
|
900
|
+
else if (rawStatus === "result" || rawStatus === "end" || rawStatus === "done" || rawStatus === "success" || rawStatus === "complete") status = "done";
|
|
672
901
|
else status = "running";
|
|
673
902
|
const summaryRaw = asString(data.summary) ?? asString(data.title) ?? asString(data.label) ?? asString(data.description);
|
|
674
903
|
const summary = summaryRaw ? summaryRaw.slice(0, 200) : void 0;
|
|
904
|
+
const truncated = {};
|
|
905
|
+
let argsText;
|
|
906
|
+
let progressText;
|
|
907
|
+
let resultText;
|
|
908
|
+
let isError;
|
|
909
|
+
if (rawStatus === "start" || rawStatus === "running" || rawStatus === "") {
|
|
910
|
+
const a = serializeArgs(data.args ?? data.arguments ?? data.input);
|
|
911
|
+
if (a) {
|
|
912
|
+
argsText = a.text;
|
|
913
|
+
if (a.truncated) truncated.args = true;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const partial = data.partialResult ?? data.partial ?? data.delta;
|
|
917
|
+
if (partial !== void 0) {
|
|
918
|
+
const p = serializeProgress(partial);
|
|
919
|
+
if (p) {
|
|
920
|
+
progressText = p.text;
|
|
921
|
+
if (p.truncated) truncated.progress = true;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const resultRaw = data.result ?? data.output;
|
|
925
|
+
if (resultRaw !== void 0 || status === "done" || status === "failed") {
|
|
926
|
+
if (resultRaw !== void 0) {
|
|
927
|
+
const r = serializeResult(resultRaw);
|
|
928
|
+
if (r) {
|
|
929
|
+
resultText = r.text;
|
|
930
|
+
if (r.truncated) truncated.result = true;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (data.isError === true || status === "failed") isError = true;
|
|
934
|
+
else if (data.isError === false) isError = false;
|
|
935
|
+
}
|
|
936
|
+
const durationRaw = data.durationMs ?? data.duration;
|
|
937
|
+
const durationMs = typeof durationRaw === "number" && Number.isFinite(durationRaw) ? durationRaw : void 0;
|
|
938
|
+
const hasTruncated = truncated.args === true || truncated.progress === true || truncated.result === true;
|
|
675
939
|
return {
|
|
676
940
|
kind: "tool",
|
|
677
941
|
toolCallId,
|
|
678
942
|
name,
|
|
679
943
|
...phase ? { phase } : {},
|
|
680
944
|
status,
|
|
681
|
-
...summary ? { summary } : {}
|
|
945
|
+
...summary ? { summary } : {},
|
|
946
|
+
...argsText !== void 0 ? { argsText } : {},
|
|
947
|
+
...progressText !== void 0 ? { progressText } : {},
|
|
948
|
+
...resultText !== void 0 ? { resultText } : {},
|
|
949
|
+
...isError !== void 0 ? { isError } : {},
|
|
950
|
+
...durationMs !== void 0 ? { durationMs } : {},
|
|
951
|
+
...hasTruncated ? { truncated } : {}
|
|
682
952
|
};
|
|
683
953
|
}
|
|
684
954
|
/**
|
|
@@ -859,6 +1129,19 @@ async function handleAgentRequest(request, log) {
|
|
|
859
1129
|
if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
|
|
860
1130
|
await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
|
|
861
1131
|
let resolvedOpenClawKey = null;
|
|
1132
|
+
const UPDATE_THROTTLE_MS = 250;
|
|
1133
|
+
const pendingUpdates = /* @__PURE__ */ new Map();
|
|
1134
|
+
const flushToolUpdate = (toolCallId) => {
|
|
1135
|
+
const pending = pendingUpdates.get(toolCallId);
|
|
1136
|
+
if (!pending) return;
|
|
1137
|
+
clearTimeout(pending.timer);
|
|
1138
|
+
pendingUpdates.delete(toolCallId);
|
|
1139
|
+
pending.send();
|
|
1140
|
+
};
|
|
1141
|
+
const clearAllToolUpdates = () => {
|
|
1142
|
+
for (const pending of pendingUpdates.values()) clearTimeout(pending.timer);
|
|
1143
|
+
pendingUpdates.clear();
|
|
1144
|
+
};
|
|
862
1145
|
const unsubscribe = runtime.events.onAgentEvent((evt) => {
|
|
863
1146
|
if (!evt.sessionKey) return;
|
|
864
1147
|
if (evt.stream === "assistant" || evt.stream === "thinking" || evt.stream === "tool") resolvedOpenClawKey ??= evt.sessionKey;
|
|
@@ -890,17 +1173,38 @@ async function handleAgentRequest(request, log) {
|
|
|
890
1173
|
}
|
|
891
1174
|
if (evt.stream === "tool") {
|
|
892
1175
|
const activity = buildToolActivity(evt.data);
|
|
893
|
-
if (activity
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1176
|
+
if (activity?.kind !== "tool") return;
|
|
1177
|
+
const toolCallId = activity.toolCallId;
|
|
1178
|
+
const send = () => {
|
|
1179
|
+
chatClient?.sendEvent("chat-activity", {
|
|
1180
|
+
runId: evt.runId,
|
|
1181
|
+
sessionKey: legacySessionKey,
|
|
1182
|
+
seq: evt.seq,
|
|
1183
|
+
state: "activity",
|
|
1184
|
+
activity: {
|
|
1185
|
+
...activity,
|
|
1186
|
+
ts: evt.ts,
|
|
1187
|
+
seq: evt.seq
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
};
|
|
1191
|
+
const rawPhase = (activity.phase ?? "").toLowerCase();
|
|
1192
|
+
if (rawPhase === "update" || activity.status === "running" && rawPhase !== "start" && rawPhase !== "") {
|
|
1193
|
+
const existing = pendingUpdates.get(toolCallId);
|
|
1194
|
+
if (existing) existing.send = send;
|
|
1195
|
+
else {
|
|
1196
|
+
const timer = setTimeout(() => {
|
|
1197
|
+
flushToolUpdate(toolCallId);
|
|
1198
|
+
}, UPDATE_THROTTLE_MS);
|
|
1199
|
+
pendingUpdates.set(toolCallId, {
|
|
1200
|
+
timer,
|
|
1201
|
+
send
|
|
1202
|
+
});
|
|
902
1203
|
}
|
|
903
|
-
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
flushToolUpdate(toolCallId);
|
|
1207
|
+
send();
|
|
904
1208
|
return;
|
|
905
1209
|
}
|
|
906
1210
|
});
|
|
@@ -1001,6 +1305,7 @@ async function handleAgentRequest(request, log) {
|
|
|
1001
1305
|
chatClient?.sendResponse(request.id, false, { message: errMsg });
|
|
1002
1306
|
} finally {
|
|
1003
1307
|
unsubscribe();
|
|
1308
|
+
clearAllToolUpdates();
|
|
1004
1309
|
if (isA2A) disarmA2AEndSignal();
|
|
1005
1310
|
}
|
|
1006
1311
|
}
|
|
@@ -1235,6 +1540,12 @@ Object.defineProperty(exports, "buildA2ACompletePayload", {
|
|
|
1235
1540
|
return buildA2ACompletePayload;
|
|
1236
1541
|
}
|
|
1237
1542
|
});
|
|
1543
|
+
Object.defineProperty(exports, "buildToolActivity", {
|
|
1544
|
+
enumerable: true,
|
|
1545
|
+
get: function() {
|
|
1546
|
+
return buildToolActivity;
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1238
1549
|
Object.defineProperty(exports, "createAlfeChannelPlugin", {
|
|
1239
1550
|
enumerable: true,
|
|
1240
1551
|
get: function() {
|
package/dist/plugin2.d.cts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
export { buildA2ACompletePayload, plugin as default, stripLeakedToolSummary };
|
|
1
|
+
import { i as stripLeakedToolSummary, n as buildToolActivity, r as plugin, t as buildA2ACompletePayload } from "./plugin.cjs";
|
|
2
|
+
export { buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
|
package/dist/plugin2.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
export { buildA2ACompletePayload, plugin as default, stripLeakedToolSummary };
|
|
1
|
+
import { i as stripLeakedToolSummary, n as buildToolActivity, r as plugin, t as buildA2ACompletePayload } from "./plugin.js";
|
|
2
|
+
export { buildA2ACompletePayload, buildToolActivity, plugin as default, stripLeakedToolSummary };
|
package/dist/plugin2.js
CHANGED
|
@@ -619,6 +619,235 @@ function validateAttachmentUrl(input, opts = {}) {
|
|
|
619
619
|
};
|
|
620
620
|
}
|
|
621
621
|
//#endregion
|
|
622
|
+
//#region src/activity-serialize.ts
|
|
623
|
+
/**
|
|
624
|
+
* Console-grade tool-activity serialization (Slice 3).
|
|
625
|
+
*
|
|
626
|
+
* The chat plugin forwards a curated activity stream to the chat service.
|
|
627
|
+
* For tool calls we now carry the args / streaming progress / final result
|
|
628
|
+
* so the client can render an OpenClaw-console-grade card. Those payloads
|
|
629
|
+
* arrive from OpenClaw's `onAgentEvent` bus as arbitrary `unknown`-shaped
|
|
630
|
+
* objects (`args` is the raw tool-call arguments; `partialResult` / `result`
|
|
631
|
+
* are tool-result envelopes). We must render them to BOUNDED strings BEFORE
|
|
632
|
+
* they cross the WS to the chat service, because:
|
|
633
|
+
*
|
|
634
|
+
* 1. Daemon WS frames are `maxPayload`-bounded — an uncapped multi-MB tool
|
|
635
|
+
* stdout would drop the WHOLE frame (losing the status transition too),
|
|
636
|
+
* so the cap has to live on the emit side, not the client.
|
|
637
|
+
* 2. The relay is a dumb passthrough and the reducer is pure — neither
|
|
638
|
+
* should have to serialize or sanitize unknown objects. The wire carries
|
|
639
|
+
* display text, never raw objects.
|
|
640
|
+
*
|
|
641
|
+
* OpenClaw's own `sanitizeToolResult` already strips standard image content
|
|
642
|
+
* blocks (`{type:'image', data}` → `{omitted:true}`) and truncates text on
|
|
643
|
+
* the `result`/`partialResult` path — but (a) its helpers are NOT importable
|
|
644
|
+
* from the plugin (openclaw is a runtime peer dep, absent from node_modules),
|
|
645
|
+
* and (b) `args` is NOT sanitized at all and can carry base64 (an inline
|
|
646
|
+
* image arg, a `data:` URI in a write). So we reimplement a defensive
|
|
647
|
+
* strip+cap here and apply it to every field, regardless of upstream
|
|
648
|
+
* sanitization.
|
|
649
|
+
*/
|
|
650
|
+
/** Args render cap — args are usually small; 4KB is generous. */
|
|
651
|
+
const ARGS_CAP = 4 * 1024;
|
|
652
|
+
/** Streaming progress window — keep the newest 4KB (tail-truncated). */
|
|
653
|
+
const PROGRESS_CAP = 4 * 1024;
|
|
654
|
+
/** Final result cap — results can be large (file reads, search); 16KB. */
|
|
655
|
+
const RESULT_CAP = 16 * 1024;
|
|
656
|
+
/**
|
|
657
|
+
* Replace long base64 runs, `data:` URIs, and known image content-block
|
|
658
|
+
* shapes with a placeholder. Runs BEFORE JSON serialization on a deep-cloned
|
|
659
|
+
* structure so we never mutate OpenClaw's live objects.
|
|
660
|
+
*
|
|
661
|
+
* Detection heuristics (all conservative — we only replace clear payloads):
|
|
662
|
+
* - A string that is a `data:` URI → `[data-uri omitted]`.
|
|
663
|
+
* - A string that is a long (≥256 char) run of base64 chars → `[base64 omitted]`.
|
|
664
|
+
* 256 is well above any realistic id/token/hash and below inline media.
|
|
665
|
+
* - An object that looks like an image content block (`type:'image'` with a
|
|
666
|
+
* `data`/`source`/`url` field, OR an Anthropic `{source:{data}}` shape) →
|
|
667
|
+
* the payload field is replaced with `[image omitted]`.
|
|
668
|
+
*/
|
|
669
|
+
const DATA_URI_RE = /^data:[^;,]*(;[^,]*)?,/i;
|
|
670
|
+
const BASE64_BLOB_RE = /^[A-Za-z0-9+/=\r\n]+$/;
|
|
671
|
+
const BASE64_MIN_LEN = 256;
|
|
672
|
+
function looksLikeBase64Blob(s) {
|
|
673
|
+
if (s.length < BASE64_MIN_LEN) return false;
|
|
674
|
+
return s.replace(/[\r\n]/g, "").length >= BASE64_MIN_LEN && BASE64_BLOB_RE.test(s);
|
|
675
|
+
}
|
|
676
|
+
const EMBEDDED_DATA_URI_RE = /data:[^;,\s"'`]{0,64}(?:;[^,\s"'`]{0,32})?,[A-Za-z0-9+/=]{128,}/g;
|
|
677
|
+
const EMBEDDED_BASE64_RE = /[A-Za-z0-9+/]{512,}={0,2}/g;
|
|
678
|
+
/**
|
|
679
|
+
* Post-match filter for EMBEDDED_BASE64_RE: real base64 media is
|
|
680
|
+
* high-entropy — a 512+ char run virtually always mixes lower, upper, and
|
|
681
|
+
* digits. This keeps legitimate long runs (a repeated fill char in test
|
|
682
|
+
* output, an ASCII divider) from being eaten by the strip.
|
|
683
|
+
*/
|
|
684
|
+
function looksHighEntropyBase64(run) {
|
|
685
|
+
return /[a-z]/.test(run) && /[A-Z]/.test(run) && /\d/.test(run);
|
|
686
|
+
}
|
|
687
|
+
function stripStringMedia(s) {
|
|
688
|
+
if (DATA_URI_RE.test(s)) return "[data-uri omitted]";
|
|
689
|
+
if (looksLikeBase64Blob(s)) return "[base64 omitted]";
|
|
690
|
+
if (s.length >= 128) return s.replace(EMBEDDED_DATA_URI_RE, "[data-uri omitted]").replace(EMBEDDED_BASE64_RE, (m) => looksHighEntropyBase64(m) ? "[base64 omitted]" : m);
|
|
691
|
+
return s;
|
|
692
|
+
}
|
|
693
|
+
const MEDIA_PAYLOAD_KEYS = new Set([
|
|
694
|
+
"data",
|
|
695
|
+
"base64",
|
|
696
|
+
"b64_json",
|
|
697
|
+
"bytes"
|
|
698
|
+
]);
|
|
699
|
+
function isImageBlock(obj) {
|
|
700
|
+
const type = typeof obj.type === "string" ? obj.type.toLowerCase() : "";
|
|
701
|
+
if (type === "image" || type === "image_url" || type === "input_image") return true;
|
|
702
|
+
if (obj.source && typeof obj.source === "object") {
|
|
703
|
+
const src = obj.source;
|
|
704
|
+
if (typeof src.data === "string" || typeof src.url === "string") return true;
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Deep-clone `value` while stripping media. Guards against cycles (returns
|
|
710
|
+
* `'[circular]'`) and caps recursion depth. Non-plain values (functions,
|
|
711
|
+
* symbols) are dropped by JSON semantics later; here we normalize them so the
|
|
712
|
+
* safe-JSON pass can't throw.
|
|
713
|
+
*/
|
|
714
|
+
function stripMediaDeep(value, seen, depth) {
|
|
715
|
+
if (depth > 12) return "[max-depth]";
|
|
716
|
+
if (value === null) return null;
|
|
717
|
+
const t = typeof value;
|
|
718
|
+
if (t === "string") return stripStringMedia(value);
|
|
719
|
+
if (t === "number" || t === "boolean") return value;
|
|
720
|
+
if (t === "bigint") return `${value.toString()}n`;
|
|
721
|
+
if (t === "undefined" || t === "function" || t === "symbol") return void 0;
|
|
722
|
+
const obj = value;
|
|
723
|
+
if (seen.has(obj)) return "[circular]";
|
|
724
|
+
seen.add(obj);
|
|
725
|
+
try {
|
|
726
|
+
if (Array.isArray(obj)) return obj.map((item) => stripMediaDeep(item, seen, depth + 1));
|
|
727
|
+
const record = obj;
|
|
728
|
+
const imageBlock = isImageBlock(record);
|
|
729
|
+
const out = {};
|
|
730
|
+
for (const [key, v] of Object.entries(record)) {
|
|
731
|
+
if (imageBlock && MEDIA_PAYLOAD_KEYS.has(key)) {
|
|
732
|
+
out[key] = "[image omitted]";
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (imageBlock && key === "source" && v && typeof v === "object") {
|
|
736
|
+
const src = { ...v };
|
|
737
|
+
for (const mk of MEDIA_PAYLOAD_KEYS) if (typeof src[mk] === "string") src[mk] = "[image omitted]";
|
|
738
|
+
out[key] = src;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
out[key] = stripMediaDeep(v, seen, depth + 1);
|
|
742
|
+
}
|
|
743
|
+
return out;
|
|
744
|
+
} finally {
|
|
745
|
+
seen.delete(obj);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Cycle- and BigInt-safe JSON stringify. Returns `undefined` when there's
|
|
750
|
+
* nothing meaningful to render (null/undefined). A plain string value is
|
|
751
|
+
* returned as-is (unquoted) — args like `"ls -la"` read better raw than
|
|
752
|
+
* `"\"ls -la\""`.
|
|
753
|
+
*/
|
|
754
|
+
function safeJson(value) {
|
|
755
|
+
if (value === null || value === void 0) return void 0;
|
|
756
|
+
const stripped = stripMediaDeep(value, /* @__PURE__ */ new WeakSet(), 0);
|
|
757
|
+
if (stripped === void 0) return void 0;
|
|
758
|
+
if (typeof stripped === "string") return stripped;
|
|
759
|
+
try {
|
|
760
|
+
return JSON.stringify(stripped, null, 2);
|
|
761
|
+
} catch {
|
|
762
|
+
return "[unserializable]";
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
/** Head-truncate: keep the START of the text (for args/results). */
|
|
766
|
+
function capHead(text, cap) {
|
|
767
|
+
if (text.length <= cap) return {
|
|
768
|
+
text,
|
|
769
|
+
truncated: false
|
|
770
|
+
};
|
|
771
|
+
return {
|
|
772
|
+
text: `${text.slice(0, cap)}\n… [truncated ${String(text.length - cap)} chars]`,
|
|
773
|
+
truncated: true
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/** Tail-truncate: keep the END of the text (for streaming progress). */
|
|
777
|
+
function capTail(text, cap) {
|
|
778
|
+
if (text.length <= cap) return {
|
|
779
|
+
text,
|
|
780
|
+
truncated: false
|
|
781
|
+
};
|
|
782
|
+
return {
|
|
783
|
+
text: `… [truncated ${String(text.length - cap)} chars]\n${text.slice(text.length - cap)}`,
|
|
784
|
+
truncated: true
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Serialize + strip + head-cap the tool-call `args` (from `phase:start`).
|
|
789
|
+
* Returns `undefined` when there's nothing to show.
|
|
790
|
+
*/
|
|
791
|
+
function serializeArgs(args) {
|
|
792
|
+
const rendered = safeJson(args);
|
|
793
|
+
if (rendered === void 0 || rendered === "" || rendered === "{}") return void 0;
|
|
794
|
+
return capHead(rendered, ARGS_CAP);
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Serialize + strip + head-cap the final tool `result` (from `phase:result`).
|
|
798
|
+
* Prefers rendering the result's text content when it's a standard
|
|
799
|
+
* `{content:[{type:'text',text}]}` envelope (reads far better than the raw
|
|
800
|
+
* JSON), falling back to the full JSON otherwise.
|
|
801
|
+
*/
|
|
802
|
+
function serializeResult(result) {
|
|
803
|
+
if (isEmptyEnvelope(result)) return void 0;
|
|
804
|
+
const rendered = extractResultText(result) ?? safeJson(result);
|
|
805
|
+
if (rendered === void 0 || rendered === "" || rendered === "{}") return void 0;
|
|
806
|
+
return capHead(stripStringMedia(rendered), RESULT_CAP);
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Serialize + strip + TAIL-cap a streaming `partialResult` (from
|
|
810
|
+
* `phase:update`). Tail-truncation keeps the NEWEST output — for a bash/exec
|
|
811
|
+
* stream the tail is what the user cares about. Whether OpenClaw's
|
|
812
|
+
* `partialResult` is cumulative or incremental is version-dependent; treating
|
|
813
|
+
* it as "the latest window" (overwrite, not append — see the reducer) is
|
|
814
|
+
* correct for both: a cumulative stream's newest frame already contains the
|
|
815
|
+
* full window (tail-capped), and an incremental stream's newest frame is the
|
|
816
|
+
* latest chunk.
|
|
817
|
+
*/
|
|
818
|
+
function serializeProgress(partial) {
|
|
819
|
+
if (isEmptyEnvelope(partial)) return void 0;
|
|
820
|
+
const rendered = extractResultText(partial) ?? safeJson(partial);
|
|
821
|
+
if (rendered === void 0 || rendered === "" || rendered === "{}") return void 0;
|
|
822
|
+
return capTail(stripStringMedia(rendered), PROGRESS_CAP);
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Pull the concatenated `text` blocks out of a standard tool-result envelope
|
|
826
|
+
* (`{content:[{type:'text',text}, {type:'image',omitted:true}]}`). Returns
|
|
827
|
+
* `undefined` when the shape isn't a text-bearing envelope, so callers fall
|
|
828
|
+
* back to full-JSON rendering.
|
|
829
|
+
*/
|
|
830
|
+
/** True for a tool-result envelope with an empty `content` array. */
|
|
831
|
+
function isEmptyEnvelope(value) {
|
|
832
|
+
if (!value || typeof value !== "object") return false;
|
|
833
|
+
const content = value.content;
|
|
834
|
+
return Array.isArray(content) && content.length === 0;
|
|
835
|
+
}
|
|
836
|
+
function extractResultText(result) {
|
|
837
|
+
if (!result || typeof result !== "object") return void 0;
|
|
838
|
+
const content = result.content;
|
|
839
|
+
if (!Array.isArray(content)) return void 0;
|
|
840
|
+
const parts = [];
|
|
841
|
+
for (const item of content) {
|
|
842
|
+
if (!item || typeof item !== "object") continue;
|
|
843
|
+
const entry = item;
|
|
844
|
+
if (entry.type === "text" && typeof entry.text === "string") parts.push(entry.text);
|
|
845
|
+
else if (entry.type === "image" || entry.omitted === true) parts.push("[image omitted]");
|
|
846
|
+
}
|
|
847
|
+
if (parts.length === 0) return void 0;
|
|
848
|
+
return parts.join("\n");
|
|
849
|
+
}
|
|
850
|
+
//#endregion
|
|
622
851
|
//#region src/plugin.ts
|
|
623
852
|
/**
|
|
624
853
|
* @alfe.ai/openclaw-chat — OpenClaw chat channel plugin.
|
|
@@ -668,17 +897,58 @@ function buildToolActivity(data) {
|
|
|
668
897
|
const rawStatus = (phase ?? "").toLowerCase();
|
|
669
898
|
let status;
|
|
670
899
|
if (rawStatus === "error" || rawStatus === "failed" || data.error != null || data.isError === true) status = "failed";
|
|
671
|
-
else if (rawStatus === "end" || rawStatus === "done" || rawStatus === "success" || rawStatus === "complete") status = "done";
|
|
900
|
+
else if (rawStatus === "result" || rawStatus === "end" || rawStatus === "done" || rawStatus === "success" || rawStatus === "complete") status = "done";
|
|
672
901
|
else status = "running";
|
|
673
902
|
const summaryRaw = asString(data.summary) ?? asString(data.title) ?? asString(data.label) ?? asString(data.description);
|
|
674
903
|
const summary = summaryRaw ? summaryRaw.slice(0, 200) : void 0;
|
|
904
|
+
const truncated = {};
|
|
905
|
+
let argsText;
|
|
906
|
+
let progressText;
|
|
907
|
+
let resultText;
|
|
908
|
+
let isError;
|
|
909
|
+
if (rawStatus === "start" || rawStatus === "running" || rawStatus === "") {
|
|
910
|
+
const a = serializeArgs(data.args ?? data.arguments ?? data.input);
|
|
911
|
+
if (a) {
|
|
912
|
+
argsText = a.text;
|
|
913
|
+
if (a.truncated) truncated.args = true;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const partial = data.partialResult ?? data.partial ?? data.delta;
|
|
917
|
+
if (partial !== void 0) {
|
|
918
|
+
const p = serializeProgress(partial);
|
|
919
|
+
if (p) {
|
|
920
|
+
progressText = p.text;
|
|
921
|
+
if (p.truncated) truncated.progress = true;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const resultRaw = data.result ?? data.output;
|
|
925
|
+
if (resultRaw !== void 0 || status === "done" || status === "failed") {
|
|
926
|
+
if (resultRaw !== void 0) {
|
|
927
|
+
const r = serializeResult(resultRaw);
|
|
928
|
+
if (r) {
|
|
929
|
+
resultText = r.text;
|
|
930
|
+
if (r.truncated) truncated.result = true;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (data.isError === true || status === "failed") isError = true;
|
|
934
|
+
else if (data.isError === false) isError = false;
|
|
935
|
+
}
|
|
936
|
+
const durationRaw = data.durationMs ?? data.duration;
|
|
937
|
+
const durationMs = typeof durationRaw === "number" && Number.isFinite(durationRaw) ? durationRaw : void 0;
|
|
938
|
+
const hasTruncated = truncated.args === true || truncated.progress === true || truncated.result === true;
|
|
675
939
|
return {
|
|
676
940
|
kind: "tool",
|
|
677
941
|
toolCallId,
|
|
678
942
|
name,
|
|
679
943
|
...phase ? { phase } : {},
|
|
680
944
|
status,
|
|
681
|
-
...summary ? { summary } : {}
|
|
945
|
+
...summary ? { summary } : {},
|
|
946
|
+
...argsText !== void 0 ? { argsText } : {},
|
|
947
|
+
...progressText !== void 0 ? { progressText } : {},
|
|
948
|
+
...resultText !== void 0 ? { resultText } : {},
|
|
949
|
+
...isError !== void 0 ? { isError } : {},
|
|
950
|
+
...durationMs !== void 0 ? { durationMs } : {},
|
|
951
|
+
...hasTruncated ? { truncated } : {}
|
|
682
952
|
};
|
|
683
953
|
}
|
|
684
954
|
/**
|
|
@@ -859,6 +1129,19 @@ async function handleAgentRequest(request, log) {
|
|
|
859
1129
|
if (!await getSession(sessionId)) await createSession(sessionId, "", "alfe", tenantId, userId);
|
|
860
1130
|
await addMessage(sessionId, "user", message, userId ?? senderId, displayName ?? senderId);
|
|
861
1131
|
let resolvedOpenClawKey = null;
|
|
1132
|
+
const UPDATE_THROTTLE_MS = 250;
|
|
1133
|
+
const pendingUpdates = /* @__PURE__ */ new Map();
|
|
1134
|
+
const flushToolUpdate = (toolCallId) => {
|
|
1135
|
+
const pending = pendingUpdates.get(toolCallId);
|
|
1136
|
+
if (!pending) return;
|
|
1137
|
+
clearTimeout(pending.timer);
|
|
1138
|
+
pendingUpdates.delete(toolCallId);
|
|
1139
|
+
pending.send();
|
|
1140
|
+
};
|
|
1141
|
+
const clearAllToolUpdates = () => {
|
|
1142
|
+
for (const pending of pendingUpdates.values()) clearTimeout(pending.timer);
|
|
1143
|
+
pendingUpdates.clear();
|
|
1144
|
+
};
|
|
862
1145
|
const unsubscribe = runtime.events.onAgentEvent((evt) => {
|
|
863
1146
|
if (!evt.sessionKey) return;
|
|
864
1147
|
if (evt.stream === "assistant" || evt.stream === "thinking" || evt.stream === "tool") resolvedOpenClawKey ??= evt.sessionKey;
|
|
@@ -890,17 +1173,38 @@ async function handleAgentRequest(request, log) {
|
|
|
890
1173
|
}
|
|
891
1174
|
if (evt.stream === "tool") {
|
|
892
1175
|
const activity = buildToolActivity(evt.data);
|
|
893
|
-
if (activity
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1176
|
+
if (activity?.kind !== "tool") return;
|
|
1177
|
+
const toolCallId = activity.toolCallId;
|
|
1178
|
+
const send = () => {
|
|
1179
|
+
chatClient?.sendEvent("chat-activity", {
|
|
1180
|
+
runId: evt.runId,
|
|
1181
|
+
sessionKey: legacySessionKey,
|
|
1182
|
+
seq: evt.seq,
|
|
1183
|
+
state: "activity",
|
|
1184
|
+
activity: {
|
|
1185
|
+
...activity,
|
|
1186
|
+
ts: evt.ts,
|
|
1187
|
+
seq: evt.seq
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
};
|
|
1191
|
+
const rawPhase = (activity.phase ?? "").toLowerCase();
|
|
1192
|
+
if (rawPhase === "update" || activity.status === "running" && rawPhase !== "start" && rawPhase !== "") {
|
|
1193
|
+
const existing = pendingUpdates.get(toolCallId);
|
|
1194
|
+
if (existing) existing.send = send;
|
|
1195
|
+
else {
|
|
1196
|
+
const timer = setTimeout(() => {
|
|
1197
|
+
flushToolUpdate(toolCallId);
|
|
1198
|
+
}, UPDATE_THROTTLE_MS);
|
|
1199
|
+
pendingUpdates.set(toolCallId, {
|
|
1200
|
+
timer,
|
|
1201
|
+
send
|
|
1202
|
+
});
|
|
902
1203
|
}
|
|
903
|
-
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
flushToolUpdate(toolCallId);
|
|
1207
|
+
send();
|
|
904
1208
|
return;
|
|
905
1209
|
}
|
|
906
1210
|
});
|
|
@@ -1001,6 +1305,7 @@ async function handleAgentRequest(request, log) {
|
|
|
1001
1305
|
chatClient?.sendResponse(request.id, false, { message: errMsg });
|
|
1002
1306
|
} finally {
|
|
1003
1307
|
unsubscribe();
|
|
1308
|
+
clearAllToolUpdates();
|
|
1004
1309
|
if (isA2A) disarmA2AEndSignal();
|
|
1005
1310
|
}
|
|
1006
1311
|
}
|
|
@@ -1229,4 +1534,4 @@ const plugin = {
|
|
|
1229
1534
|
}
|
|
1230
1535
|
};
|
|
1231
1536
|
//#endregion
|
|
1232
|
-
export { createAlfeChannelPlugin as i,
|
|
1537
|
+
export { createAlfeChannelPlugin as a, stripLeakedToolSummary as i, buildToolActivity as n, plugin as r, buildA2ACompletePayload as t };
|