fix: prevent history truncation from orphaning tool results
This commit is contained in:
parent
14e4b88bf0
commit
bed8b67246
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user