Limit tool call coalescing to prepend boundary

This commit is contained in:
Mohamed Boudra
2026-05-17 22:24:59 +07:00
parent 338a41991d
commit 1f7fc232b3
3 changed files with 100 additions and 28 deletions
@@ -601,6 +601,84 @@ describe("processTimelineResponse", () => {
]);
});
it("does not coalesce tool call lifecycle rows away from the prepend boundary", () => {
const callId = "toolu_not_boundary";
const currentTail = hydrateStreamState(
[
{
event: makeTimelineEvent("current chunk"),
timestamp: new Date(3000),
},
{
event: {
type: "timeline",
provider: "claude",
item: makeToolCallTimelineEntry(4, callId, "completed", {
type: "read",
filePath: "/tmp/example.ts",
}).item,
} as AgentStreamEventPayload,
timestamp: new Date(4000),
},
],
{ source: "canonical" },
);
const existingCursor: TimelineCursor = {
epoch: "epoch-1",
startSeq: 3,
endSeq: 5,
};
const result = processTimelineResponse({
...baseTimelineInput,
currentTail,
currentCursor: existingCursor,
payload: {
...baseTimelineInput.payload,
direction: "before",
epoch: "epoch-1",
startCursor: { seq: 1 },
endCursor: { seq: 2 },
entries: [
makeToolCallTimelineEntry(1, callId, "running", {
type: "unknown",
input: { file_path: "/tmp/example.ts" },
output: null,
}),
makeTimelineEntry(2, "older chunk "),
],
},
});
expect(
result.tail.map((item) => ({
kind: item.kind,
id: item.id,
status: item.kind === "tool_call" ? item.payload.data.status : null,
text: item.kind === "assistant_message" ? item.text : null,
})),
).toEqual([
{
kind: "tool_call",
id: `agent_tool_${callId}`,
status: "running",
text: null,
},
{
kind: "assistant_message",
id: expect.any(String),
status: null,
text: "older chunk current chunk",
},
{
kind: "tool_call",
id: `agent_tool_${callId}`,
status: "completed",
text: null,
},
]);
});
it("requests canonical catch-up when a projected entry overlaps unseen seqs", () => {
const existingCursor: TimelineCursor = {
epoch: "epoch-1",
@@ -5,8 +5,9 @@ import { useSessionStore } from "@/stores/session-store";
import type { StreamItem } from "@/types/stream";
import {
applyStreamEvent,
coalesceAgentToolCallItems,
hydrateStreamState,
isAgentToolCallItem,
mergeAgentToolCallItem,
reduceStreamUpdate,
} from "@/types/stream";
@@ -351,11 +352,26 @@ function mergePrependedCanonicalTail(olderTail: StreamItem[], currentTail: Strea
const olderLast = olderTail.at(-1);
const currentFirst = currentTail[0];
if (olderLast?.kind !== "assistant_message" || currentFirst?.kind !== "assistant_message") {
return coalesceAgentToolCallItems([...olderTail, ...currentTail]);
if (
olderLast &&
currentFirst &&
isAgentToolCallItem(olderLast) &&
isAgentToolCallItem(currentFirst) &&
olderLast.payload.data.callId === currentFirst.payload.data.callId
) {
return [
...olderTail.slice(0, -1),
mergeAgentToolCallItem(olderLast, currentFirst.payload.data, currentFirst.timestamp),
...currentTail.slice(1),
];
}
return coalesceAgentToolCallItems([
if (olderLast?.kind !== "assistant_message" || currentFirst?.kind !== "assistant_message") {
return [...olderTail, ...currentTail];
}
return [
...olderTail.slice(0, -1),
{
...olderLast,
@@ -363,7 +379,7 @@ function mergePrependedCanonicalTail(olderTail: StreamItem[], currentTail: Strea
timestamp: currentFirst.timestamp,
},
...currentTail.slice(1),
]);
];
}
function applyTimelineIncrementalPath(args: {
+1 -23
View File
@@ -438,7 +438,7 @@ function mergeAgentToolCallStatus(
return "running";
}
function mergeAgentToolCallItem(
export function mergeAgentToolCallItem(
existing: AgentToolCallItem,
data: AgentToolCallData,
timestamp: Date,
@@ -515,28 +515,6 @@ function appendAgentToolCall(
return [...state, item];
}
export function coalesceAgentToolCallItems(items: StreamItem[]): StreamItem[] {
const state: StreamItem[] = [];
const toolCallIndexByCallId = new Map<string, number>();
for (const item of items) {
if (!isAgentToolCallItem(item)) {
state.push(item);
continue;
}
const existingIndex = toolCallIndexByCallId.get(item.payload.data.callId);
if (existingIndex === undefined) {
toolCallIndexByCallId.set(item.payload.data.callId, state.length);
state.push(item);
continue;
}
const existing = state[existingIndex];
if (existing && isAgentToolCallItem(existing)) {
state[existingIndex] = mergeAgentToolCallItem(existing, item.payload.data, item.timestamp);
}
}
return state;
}
function appendActivityLog(state: StreamItem[], entry: ActivityLogItem): StreamItem[] {
const index = state.findIndex((existing) => existing.id === entry.id);
if (index >= 0) {