fix: prevent history truncation from orphaning tool results

This commit is contained in:
Kastrah 2026-01-27 15:58:13 +01:00
parent 14e4b88bf0
commit bed8b67246
2 changed files with 43 additions and 2 deletions

View File

@ -154,7 +154,27 @@ describe("limitHistoryTurns", () => {
{ role: "assistant", content: [{ type: "text", text: "response" }] },
];
const limited = limitHistoryTurns(messages, 1);
expect(limited[0].content).toEqual([{ type: "text", text: "second" }]);
expect(limited[1].content).toEqual([{ type: "text", text: "response" }]);
});
it("does not slice between tool use and tool result when limit cuts off tool use", () => {
const messages: AgentMessage[] = [
{ role: "user", content: [{ type: "text", text: "start" }] },
{ role: "assistant", content: [{ type: "text", text: "ack" }] },
{ role: "user", content: [{ type: "text", text: "do tool" }] },
{ role: "assistant", content: [{ type: "tool_use", id: "call_1", name: "foo", input: {} }] },
{ role: "user", content: [{ type: "tool_result", tool_use_id: "call_1", content: "res" }] },
];
// If we limit to 1 turn, we should get the full tool interaction chain (User -> Asst(Call) -> User(Result))
const limited = limitHistoryTurns(messages, 1);
expect(limited.length).toBe(3);
expect(limited[0].role).toBe("user");
expect((limited[0].content as any)[0].text).toBe("do tool");
expect(limited[1].role).toBe("assistant");
expect((limited[1].content as any)[0].type).toBe("tool_use");
expect(limited[2].role).toBe("user");
expect((limited[2].content as any)[0].type).toBe("tool_result");
});
});

View File

@ -9,9 +9,28 @@ function stripThreadSuffix(value: string): string {
return match?.[1] ?? value;
}
/**
* Check if a user message is purely a tool result (not a new user turn).
*/
function isToolResultMessage(msg: AgentMessage): boolean {
if (msg.role !== "user") return false;
const content = msg.content;
if (!Array.isArray(content)) return false;
// A tool result message contains only tool_result blocks
return (
content.length > 0 &&
content.every((block) => {
if (!block || typeof block !== "object") return false;
const type = (block as { type?: unknown }).type;
return type === "tool_result";
})
);
}
/**
* Limits conversation history to the last N user turns (and their associated
* assistant responses). This reduces token usage for long-running DM sessions.
* Tool result messages are not counted as new user turns.
*/
export function limitHistoryTurns(
messages: AgentMessage[],
@ -23,7 +42,9 @@ export function limitHistoryTurns(
let lastUserIndex = messages.length;
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role === "user") {
const msg = messages[i];
// Only count genuine user messages, not tool results
if (msg.role === "user" && !isToolResultMessage(msg)) {
userCount++;
if (userCount > limit) {
return messages.slice(lastUserIndex);