Skip to content

Commit 64402af

Browse files
authored
fix(session): validate role order before trimming tool call pair (#98)
1 parent 9a78927 commit 64402af

2 files changed

Lines changed: 35 additions & 2 deletions

File tree

packages/core/src/__tests__/session/conversation.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,28 @@ describe("addTurn", () => {
6363
expect(history[2].content).toBe("new response");
6464
});
6565

66+
it("does not orphan a tool_result when an unpaired tool_use precedes a complete pair", () => {
67+
// Scenario: abort interrupted a previous run between tool_use(A) and
68+
// tool_result(A), leaving an orphaned assistant tool_use. A later run
69+
// completed a full tool_use(B) → tool_result(B) pair. Trimming must
70+
// drop only the orphan, not consume tool_use(B) as if it were
71+
// tool_result(A).
72+
const history: ConversationTurn[] = [
73+
{ role: "assistant", content: "tool_use(A)", isToolCall: true },
74+
{ role: "assistant", content: "tool_use(B)", isToolCall: true },
75+
{ role: "user", content: "tool_result(B)", isToolCall: true },
76+
{ role: "assistant", content: "response" },
77+
];
78+
79+
addTurn(history, { role: "user", content: "next" }, 4);
80+
81+
expect(history).toHaveLength(4);
82+
expect(history[0].content).toBe("tool_use(B)");
83+
expect(history[1].content).toBe("tool_result(B)");
84+
expect(history[2].content).toBe("response");
85+
expect(history[3].content).toBe("next");
86+
});
87+
6688
it("does not split a tool call pair when trimming", () => {
6789
const history: ConversationTurn[] = [
6890
{ role: "user", content: "regular" },

packages/core/src/session/conversation.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,19 @@ export function addTurn(
1515
history.push(turn);
1616

1717
while (history.length > maxTurns) {
18-
if (history[0]?.isToolCall && history[1]?.isToolCall) {
19-
// Remove both turns of the tool call pair
18+
const first = history[0];
19+
const second = history[1];
20+
// A valid tool call pair is `assistant` (tool_use) followed by `user`
21+
// (tool_result). Two consecutive isToolCall turns with the wrong roles
22+
// (e.g. two orphaned assistant tool_use entries left by an abort) must
23+
// not be removed together — that would consume an unrelated entry and
24+
// orphan its partner.
25+
if (
26+
first?.isToolCall &&
27+
second?.isToolCall &&
28+
first.role === "assistant" &&
29+
second.role === "user"
30+
) {
2031
history.splice(0, 2);
2132
} else {
2233
history.splice(0, 1);

0 commit comments

Comments
 (0)