Limit tool call coalescing to prepend boundary
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user