diff --git a/libs/deepagents/src/middleware/fs.eviction.test.ts b/libs/deepagents/src/middleware/fs.eviction.test.ts index 40f686cae..112f4cbce 100644 --- a/libs/deepagents/src/middleware/fs.eviction.test.ts +++ b/libs/deepagents/src/middleware/fs.eviction.test.ts @@ -5,6 +5,7 @@ import type { StructuredTool } from "langchain"; import { createContentPreview, createFilesystemMiddleware, + extractTextContent, TOOLS_EXCLUDED_FROM_EVICTION, NUM_CHARS_PER_TOKEN, } from "./fs.js"; @@ -293,3 +294,41 @@ describe("read_file character-based truncation", () => { expect(result.length).toBeGreaterThan(100000); }); }); + +describe("extractTextContent", () => { + it("should return the string directly for string content", () => { + expect(extractTextContent("hello world")).toBe("hello world"); + }); + + it("should join text blocks from array content", () => { + const content = [ + { type: "text", text: "hello " }, + { type: "text", text: "world" }, + ]; + expect(extractTextContent(content)).toBe("hello world"); + }); + + it("should ignore non-text blocks", () => { + const content = [ + { type: "text", text: "hello" }, + { type: "image_url", image_url: { url: "http://example.com/img.png" } }, + { type: "text", text: " world" }, + ]; + expect(extractTextContent(content)).toBe("hello world"); + }); + + it("should return null for array with no text blocks", () => { + const content = [ + { type: "image_url", image_url: { url: "http://example.com/img.png" } }, + ]; + expect(extractTextContent(content)).toBeNull(); + }); + + it("should return null for empty array", () => { + expect(extractTextContent([])).toBeNull(); + }); + + it("should return empty string for string content that is empty", () => { + expect(extractTextContent("")).toBe(""); + }); +}); diff --git a/libs/deepagents/src/middleware/fs.ts b/libs/deepagents/src/middleware/fs.ts index 1464b8872..409a15e92 100644 --- a/libs/deepagents/src/middleware/fs.ts +++ b/libs/deepagents/src/middleware/fs.ts @@ -135,6 +135,34 @@ export function createContentPreview( return headSample + truncationNotice + tailSample; } +/** + * Extract joined text from message content (string or array of content blocks). + */ +export function extractTextContent( + content: string | Array>, +): string | null { + if (typeof content === "string") { + return content; + } + if (Array.isArray(content)) { + const textParts: string[] = []; + for (const block of content) { + if ( + typeof block === "object" && + block !== null && + block.type === "text" && + typeof block.text === "string" + ) { + textParts.push(block.text); + } + } + if (textParts.length > 0) { + return textParts.join(""); + } + } + return null; +} + /** * required for type inference */ @@ -853,9 +881,10 @@ export function createFilesystemMiddleware( msg: ToolMessage, toolTokenLimitBeforeEvict: number, ) { + const textContent = extractTextContent(msg.content); if ( - typeof msg.content === "string" && - msg.content.length > toolTokenLimitBeforeEvict * NUM_CHARS_PER_TOKEN + textContent !== null && + textContent.length > toolTokenLimitBeforeEvict * NUM_CHARS_PER_TOKEN ) { // Build StateAndStore from request const stateAndStore: StateAndStore = { @@ -871,7 +900,7 @@ export function createFilesystemMiddleware( const writeResult = await resolvedBackend.write( evictPath, - msg.content, + textContent, ); if (writeResult.error) { @@ -879,7 +908,7 @@ export function createFilesystemMiddleware( } // Create preview showing head and tail of the result - const contentSample = createContentPreview(msg.content); + const contentSample = createContentPreview(textContent); const replacementText = TOO_LARGE_TOOL_MSG.replace( "{tool_call_id}", msg.tool_call_id, diff --git a/libs/deepagents/src/middleware/index.ts b/libs/deepagents/src/middleware/index.ts index be585143c..3776a4383 100644 --- a/libs/deepagents/src/middleware/index.ts +++ b/libs/deepagents/src/middleware/index.ts @@ -5,6 +5,7 @@ export { TOOLS_EXCLUDED_FROM_EVICTION, NUM_CHARS_PER_TOKEN, createContentPreview, + extractTextContent, } from "./fs.js"; export { createSubAgentMiddleware,