refactor(agents): share text block extraction helper

This commit is contained in:
Peter Steinberger
2026-02-18 18:01:09 +00:00
parent 2d55cc446a
commit 85ebdf88b0
5 changed files with 42 additions and 41 deletions

View File

@@ -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([]);
});
});

View File

@@ -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;
}

View File

@@ -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<string, unknown>;
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<string, unknown>;
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));

View File

@@ -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[] {

View File

@@ -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;