diff --git a/src/agents/content-blocks.test.ts b/src/agents/content-blocks.test.ts new file mode 100644 index 0000000000..42ec8f84c1 --- /dev/null +++ b/src/agents/content-blocks.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { collectTextContentBlocks } from "./content-blocks.js"; + +describe("collectTextContentBlocks", () => { + it("collects text content blocks in order", () => { + const blocks = [ + { type: "text", text: "first" }, + { type: "image", data: "abc" }, + { type: "text", text: "second" }, + ]; + + expect(collectTextContentBlocks(blocks)).toEqual(["first", "second"]); + }); + + it("ignores invalid entries and non-arrays", () => { + expect(collectTextContentBlocks(null)).toEqual([]); + expect(collectTextContentBlocks([{ type: "text", text: 1 }, undefined, "x"])).toEqual([]); + }); +}); diff --git a/src/agents/content-blocks.ts b/src/agents/content-blocks.ts new file mode 100644 index 0000000000..7f7dff0012 --- /dev/null +++ b/src/agents/content-blocks.ts @@ -0,0 +1,16 @@ +export function collectTextContentBlocks(content: unknown): string[] { + if (!Array.isArray(content)) { + return []; + } + const parts: string[] = []; + for (const block of content) { + if (!block || typeof block !== "object") { + continue; + } + const rec = block as { type?: unknown; text?: unknown }; + if (rec.type === "text" && typeof rec.text === "string") { + parts.push(rec.text); + } + } + return parts; +} diff --git a/src/agents/pi-embedded-subscribe.tools.ts b/src/agents/pi-embedded-subscribe.tools.ts index 6b8cd3219e..f37974cc90 100644 --- a/src/agents/pi-embedded-subscribe.tools.ts +++ b/src/agents/pi-embedded-subscribe.tools.ts @@ -2,6 +2,7 @@ import { getChannelPlugin, normalizeChannelId } from "../channels/plugins/index. import { normalizeTargetForProvider } from "../infra/outbound/target-normalization.js"; import { MEDIA_TOKEN_RE } from "../media/parse.js"; import { truncateUtf16Safe } from "../utils.js"; +import { collectTextContentBlocks } from "./content-blocks.js"; import { type MessagingToolSend } from "./pi-embedded-messaging.js"; const TOOL_RESULT_MAX_CHARS = 8000; @@ -96,20 +97,9 @@ export function extractToolResultText(result: unknown): string | undefined { return undefined; } const record = result as Record; - const content = Array.isArray(record.content) ? record.content : null; - if (!content) { - return undefined; - } - const texts = content + const texts = collectTextContentBlocks(record.content) .map((item) => { - if (!item || typeof item !== "object") { - return undefined; - } - const entry = item as Record; - if (entry.type !== "text" || typeof entry.text !== "string") { - return undefined; - } - const trimmed = entry.text.trim(); + const trimmed = item.trim(); return trimmed ? trimmed : undefined; }) .filter((value): value is string => Boolean(value)); diff --git a/src/agents/pi-extensions/compaction-safeguard.ts b/src/agents/pi-extensions/compaction-safeguard.ts index a513c88c51..12c6627e40 100644 --- a/src/agents/pi-extensions/compaction-safeguard.ts +++ b/src/agents/pi-extensions/compaction-safeguard.ts @@ -14,6 +14,7 @@ import { resolveContextWindowTokens, summarizeInStages, } from "../compaction.js"; +import { collectTextContentBlocks } from "../content-blocks.js"; import { getCompactionSafeguardRuntime } from "./compaction-safeguard-runtime.js"; const FALLBACK_SUMMARY = "Summary unavailable due to context limits. Older messages were truncated."; @@ -62,20 +63,7 @@ function formatToolFailureMeta(details: unknown): string | undefined { } function extractToolResultText(content: unknown): string { - if (!Array.isArray(content)) { - return ""; - } - const parts: string[] = []; - for (const block of content) { - if (!block || typeof block !== "object") { - continue; - } - const rec = block as { type?: unknown; text?: unknown }; - if (rec.type === "text" && typeof rec.text === "string") { - parts.push(rec.text); - } - } - return parts.join("\n"); + return collectTextContentBlocks(content).join("\n"); } function collectToolFailures(messages: AgentMessage[]): ToolFailure[] { diff --git a/src/auto-reply/reply/get-reply-inline-actions.ts b/src/auto-reply/reply/get-reply-inline-actions.ts index f579bd9212..9fdd0caf74 100644 --- a/src/auto-reply/reply/get-reply-inline-actions.ts +++ b/src/auto-reply/reply/get-reply-inline-actions.ts @@ -1,3 +1,4 @@ +import { collectTextContentBlocks } from "../../agents/content-blocks.js"; import { createOpenClawTools } from "../../agents/openclaw-tools.js"; import type { SkillCommandSpec } from "../../agents/skills.js"; import { getChannelDock } from "../../channels/dock.js"; @@ -62,20 +63,7 @@ function extractTextFromToolResult(result: any): string | null { const trimmed = content.trim(); return trimmed ? trimmed : null; } - if (!Array.isArray(content)) { - return null; - } - - const parts: string[] = []; - for (const block of content) { - if (!block || typeof block !== "object") { - continue; - } - const rec = block as { type?: unknown; text?: unknown }; - if (rec.type === "text" && typeof rec.text === "string") { - parts.push(rec.text); - } - } + const parts = collectTextContentBlocks(content); const out = parts.join(""); const trimmed = out.trim(); return trimmed ? trimmed : null;