Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit a357c00

Browse files
daniel-lxshannesrudolph
authored andcommitted
fix: transform tool blocks to text before condensing (EXT-624) (#10975)
1 parent e597921 commit a357c00

2 files changed

Lines changed: 409 additions & 2 deletions

File tree

src/core/condense/__tests__/index.spec.ts

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import {
1515
cleanupAfterTruncation,
1616
extractCommandBlocks,
1717
injectSyntheticToolResults,
18+
toolUseToText,
19+
toolResultToText,
20+
convertToolBlocksToText,
21+
transformMessagesForCondensing,
1822
} from "../index"
1923

2024
vi.mock("../../../api/transform/image-cleaning", () => ({
@@ -1282,3 +1286,306 @@ describe("summarizeConversation with custom settings", () => {
12821286
)
12831287
})
12841288
})
1289+
1290+
describe("toolUseToText", () => {
1291+
it("should convert tool_use block with object input to text", () => {
1292+
const block: Anthropic.Messages.ToolUseBlockParam = {
1293+
type: "tool_use",
1294+
id: "tool-123",
1295+
name: "read_file",
1296+
input: { path: "test.ts", encoding: "utf-8" },
1297+
}
1298+
1299+
const result = toolUseToText(block)
1300+
1301+
expect(result).toBe("[Tool Use: read_file]\npath: test.ts\nencoding: utf-8")
1302+
})
1303+
1304+
it("should convert tool_use block with nested object input to text", () => {
1305+
const block: Anthropic.Messages.ToolUseBlockParam = {
1306+
type: "tool_use",
1307+
id: "tool-456",
1308+
name: "write_file",
1309+
input: {
1310+
path: "output.json",
1311+
content: { key: "value", nested: { a: 1 } },
1312+
},
1313+
}
1314+
1315+
const result = toolUseToText(block)
1316+
1317+
expect(result).toContain("[Tool Use: write_file]")
1318+
expect(result).toContain("path: output.json")
1319+
expect(result).toContain("content:")
1320+
expect(result).toContain('"key"')
1321+
expect(result).toContain('"value"')
1322+
})
1323+
1324+
it("should convert tool_use block with string input to text", () => {
1325+
const block: Anthropic.Messages.ToolUseBlockParam = {
1326+
type: "tool_use",
1327+
id: "tool-789",
1328+
name: "execute_command",
1329+
input: "ls -la" as unknown as Record<string, unknown>,
1330+
}
1331+
1332+
const result = toolUseToText(block)
1333+
1334+
expect(result).toBe("[Tool Use: execute_command]\nls -la")
1335+
})
1336+
1337+
it("should handle empty object input", () => {
1338+
const block: Anthropic.Messages.ToolUseBlockParam = {
1339+
type: "tool_use",
1340+
id: "tool-empty",
1341+
name: "some_tool",
1342+
input: {},
1343+
}
1344+
1345+
const result = toolUseToText(block)
1346+
1347+
expect(result).toBe("[Tool Use: some_tool]\n")
1348+
})
1349+
})
1350+
1351+
describe("toolResultToText", () => {
1352+
it("should convert tool_result with string content to text", () => {
1353+
const block: Anthropic.Messages.ToolResultBlockParam = {
1354+
type: "tool_result",
1355+
tool_use_id: "tool-123",
1356+
content: "File contents here",
1357+
}
1358+
1359+
const result = toolResultToText(block)
1360+
1361+
expect(result).toBe("[Tool Result]\nFile contents here")
1362+
})
1363+
1364+
it("should convert tool_result with error flag to text", () => {
1365+
const block: Anthropic.Messages.ToolResultBlockParam = {
1366+
type: "tool_result",
1367+
tool_use_id: "tool-456",
1368+
content: "File not found",
1369+
is_error: true,
1370+
}
1371+
1372+
const result = toolResultToText(block)
1373+
1374+
expect(result).toBe("[Tool Result (Error)]\nFile not found")
1375+
})
1376+
1377+
it("should convert tool_result with array content to text", () => {
1378+
const block: Anthropic.Messages.ToolResultBlockParam = {
1379+
type: "tool_result",
1380+
tool_use_id: "tool-789",
1381+
content: [
1382+
{ type: "text", text: "First line" },
1383+
{ type: "text", text: "Second line" },
1384+
],
1385+
}
1386+
1387+
const result = toolResultToText(block)
1388+
1389+
expect(result).toBe("[Tool Result]\nFirst line\nSecond line")
1390+
})
1391+
1392+
it("should handle tool_result with image in array content", () => {
1393+
const block: Anthropic.Messages.ToolResultBlockParam = {
1394+
type: "tool_result",
1395+
tool_use_id: "tool-img",
1396+
content: [
1397+
{ type: "text", text: "Screenshot:" },
1398+
{ type: "image", source: { type: "base64", media_type: "image/png", data: "abc123" } },
1399+
],
1400+
}
1401+
1402+
const result = toolResultToText(block)
1403+
1404+
expect(result).toBe("[Tool Result]\nScreenshot:\n[Image]")
1405+
})
1406+
1407+
it("should handle tool_result with no content", () => {
1408+
const block: Anthropic.Messages.ToolResultBlockParam = {
1409+
type: "tool_result",
1410+
tool_use_id: "tool-empty",
1411+
}
1412+
1413+
const result = toolResultToText(block)
1414+
1415+
expect(result).toBe("[Tool Result]")
1416+
})
1417+
})
1418+
1419+
describe("convertToolBlocksToText", () => {
1420+
it("should return string content unchanged", () => {
1421+
const content = "Simple text content"
1422+
1423+
const result = convertToolBlocksToText(content)
1424+
1425+
expect(result).toBe("Simple text content")
1426+
})
1427+
1428+
it("should convert tool_use blocks to text blocks", () => {
1429+
const content: Anthropic.Messages.ContentBlockParam[] = [
1430+
{
1431+
type: "tool_use",
1432+
id: "tool-123",
1433+
name: "read_file",
1434+
input: { path: "test.ts" },
1435+
},
1436+
]
1437+
1438+
const result = convertToolBlocksToText(content)
1439+
1440+
expect(Array.isArray(result)).toBe(true)
1441+
expect((result as Anthropic.Messages.ContentBlockParam[])[0].type).toBe("text")
1442+
expect((result as Anthropic.Messages.TextBlockParam[])[0].text).toContain("[Tool Use: read_file]")
1443+
})
1444+
1445+
it("should convert tool_result blocks to text blocks", () => {
1446+
const content: Anthropic.Messages.ContentBlockParam[] = [
1447+
{
1448+
type: "tool_result",
1449+
tool_use_id: "tool-123",
1450+
content: "File contents",
1451+
},
1452+
]
1453+
1454+
const result = convertToolBlocksToText(content)
1455+
1456+
expect(Array.isArray(result)).toBe(true)
1457+
expect((result as Anthropic.Messages.ContentBlockParam[])[0].type).toBe("text")
1458+
expect((result as Anthropic.Messages.TextBlockParam[])[0].text).toContain("[Tool Result]")
1459+
})
1460+
1461+
it("should preserve non-tool blocks unchanged", () => {
1462+
const content: Anthropic.Messages.ContentBlockParam[] = [
1463+
{ type: "text", text: "Hello" },
1464+
{
1465+
type: "tool_use",
1466+
id: "tool-123",
1467+
name: "read_file",
1468+
input: { path: "test.ts" },
1469+
},
1470+
{ type: "text", text: "World" },
1471+
]
1472+
1473+
const result = convertToolBlocksToText(content)
1474+
1475+
expect(Array.isArray(result)).toBe(true)
1476+
const resultArray = result as Anthropic.Messages.ContentBlockParam[]
1477+
expect(resultArray).toHaveLength(3)
1478+
expect(resultArray[0]).toEqual({ type: "text", text: "Hello" })
1479+
expect(resultArray[1].type).toBe("text")
1480+
expect((resultArray[1] as Anthropic.Messages.TextBlockParam).text).toContain("[Tool Use: read_file]")
1481+
expect(resultArray[2]).toEqual({ type: "text", text: "World" })
1482+
})
1483+
1484+
it("should handle mixed content with multiple tool blocks", () => {
1485+
const content: Anthropic.Messages.ContentBlockParam[] = [
1486+
{
1487+
type: "tool_use",
1488+
id: "tool-1",
1489+
name: "read_file",
1490+
input: { path: "a.ts" },
1491+
},
1492+
{
1493+
type: "tool_result",
1494+
tool_use_id: "tool-1",
1495+
content: "contents of a.ts",
1496+
},
1497+
]
1498+
1499+
const result = convertToolBlocksToText(content)
1500+
1501+
expect(Array.isArray(result)).toBe(true)
1502+
const resultArray = result as Anthropic.Messages.ContentBlockParam[]
1503+
expect(resultArray).toHaveLength(2)
1504+
expect((resultArray[0] as Anthropic.Messages.TextBlockParam).text).toContain("[Tool Use: read_file]")
1505+
expect((resultArray[1] as Anthropic.Messages.TextBlockParam).text).toContain("[Tool Result]")
1506+
expect((resultArray[1] as Anthropic.Messages.TextBlockParam).text).toContain("contents of a.ts")
1507+
})
1508+
})
1509+
1510+
describe("transformMessagesForCondensing", () => {
1511+
it("should transform all messages with tool blocks to text", () => {
1512+
const messages = [
1513+
{ role: "user" as const, content: "Hello" },
1514+
{
1515+
role: "assistant" as const,
1516+
content: [
1517+
{
1518+
type: "tool_use" as const,
1519+
id: "tool-1",
1520+
name: "read_file",
1521+
input: { path: "test.ts" },
1522+
},
1523+
],
1524+
},
1525+
{
1526+
role: "user" as const,
1527+
content: [
1528+
{
1529+
type: "tool_result" as const,
1530+
tool_use_id: "tool-1",
1531+
content: "file contents",
1532+
},
1533+
],
1534+
},
1535+
]
1536+
1537+
const result = transformMessagesForCondensing(messages)
1538+
1539+
expect(result).toHaveLength(3)
1540+
expect(result[0].content).toBe("Hello")
1541+
expect(Array.isArray(result[1].content)).toBe(true)
1542+
expect((result[1].content as any[])[0].type).toBe("text")
1543+
expect((result[1].content as any[])[0].text).toContain("[Tool Use: read_file]")
1544+
expect(Array.isArray(result[2].content)).toBe(true)
1545+
expect((result[2].content as any[])[0].type).toBe("text")
1546+
expect((result[2].content as any[])[0].text).toContain("[Tool Result]")
1547+
})
1548+
1549+
it("should preserve message role and other properties", () => {
1550+
const messages = [
1551+
{
1552+
role: "assistant" as const,
1553+
content: [
1554+
{
1555+
type: "tool_use" as const,
1556+
id: "tool-1",
1557+
name: "execute",
1558+
input: { cmd: "ls" },
1559+
},
1560+
],
1561+
},
1562+
]
1563+
1564+
const result = transformMessagesForCondensing(messages)
1565+
1566+
expect(result[0].role).toBe("assistant")
1567+
})
1568+
1569+
it("should handle empty messages array", () => {
1570+
const result = transformMessagesForCondensing([])
1571+
1572+
expect(result).toEqual([])
1573+
})
1574+
1575+
it("should not mutate original messages", () => {
1576+
const originalContent = [
1577+
{
1578+
type: "tool_use" as const,
1579+
id: "tool-1",
1580+
name: "read_file",
1581+
input: { path: "test.ts" },
1582+
},
1583+
]
1584+
const messages = [{ role: "assistant" as const, content: originalContent }]
1585+
1586+
transformMessagesForCondensing(messages)
1587+
1588+
// Original should still have tool_use type
1589+
expect(messages[0].content[0].type).toBe("tool_use")
1590+
})
1591+
})

0 commit comments

Comments
 (0)