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", () => {
|
it("requests canonical catch-up when a projected entry overlaps unseen seqs", () => {
|
||||||
const existingCursor: TimelineCursor = {
|
const existingCursor: TimelineCursor = {
|
||||||
epoch: "epoch-1",
|
epoch: "epoch-1",
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { useSessionStore } from "@/stores/session-store";
|
|||||||
import type { StreamItem } from "@/types/stream";
|
import type { StreamItem } from "@/types/stream";
|
||||||
import {
|
import {
|
||||||
applyStreamEvent,
|
applyStreamEvent,
|
||||||
coalesceAgentToolCallItems,
|
|
||||||
hydrateStreamState,
|
hydrateStreamState,
|
||||||
|
isAgentToolCallItem,
|
||||||
|
mergeAgentToolCallItem,
|
||||||
reduceStreamUpdate,
|
reduceStreamUpdate,
|
||||||
} from "@/types/stream";
|
} from "@/types/stream";
|
||||||
|
|
||||||
@@ -351,11 +352,26 @@ function mergePrependedCanonicalTail(olderTail: StreamItem[], currentTail: Strea
|
|||||||
|
|
||||||
const olderLast = olderTail.at(-1);
|
const olderLast = olderTail.at(-1);
|
||||||
const currentFirst = currentTail[0];
|
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),
|
...olderTail.slice(0, -1),
|
||||||
{
|
{
|
||||||
...olderLast,
|
...olderLast,
|
||||||
@@ -363,7 +379,7 @@ function mergePrependedCanonicalTail(olderTail: StreamItem[], currentTail: Strea
|
|||||||
timestamp: currentFirst.timestamp,
|
timestamp: currentFirst.timestamp,
|
||||||
},
|
},
|
||||||
...currentTail.slice(1),
|
...currentTail.slice(1),
|
||||||
]);
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyTimelineIncrementalPath(args: {
|
function applyTimelineIncrementalPath(args: {
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ function mergeAgentToolCallStatus(
|
|||||||
return "running";
|
return "running";
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeAgentToolCallItem(
|
export function mergeAgentToolCallItem(
|
||||||
existing: AgentToolCallItem,
|
existing: AgentToolCallItem,
|
||||||
data: AgentToolCallData,
|
data: AgentToolCallData,
|
||||||
timestamp: Date,
|
timestamp: Date,
|
||||||
@@ -515,28 +515,6 @@ function appendAgentToolCall(
|
|||||||
return [...state, item];
|
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[] {
|
function appendActivityLog(state: StreamItem[], entry: ActivityLogItem): StreamItem[] {
|
||||||
const index = state.findIndex((existing) => existing.id === entry.id);
|
const index = state.findIndex((existing) => existing.id === entry.id);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user