Coalesce prepended tool call history

This commit is contained in:
Mohamed Boudra
2026-05-17 20:53:03 +07:00
parent e27734b218
commit c4e45af565
3 changed files with 163 additions and 34 deletions
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import type { AgentStreamEventPayload } from "@server/shared/messages";
import type { StreamItem } from "@/types/stream";
import { hydrateStreamState, type AgentToolCallItem, type StreamItem } from "@/types/stream";
import {
createAgentStreamReducerQueue,
processTimelineResponse,
@@ -31,6 +31,28 @@ function makeTimelineEntry(
};
}
function makeToolCallTimelineEntry(
seq: number,
callId: string,
status: "running" | "completed",
detail: Record<string, unknown>,
) {
return {
seqStart: seq,
seqEnd: seq,
provider: "claude",
item: {
type: "tool_call",
callId,
name: "Read",
status,
detail,
error: null,
},
timestamp: new Date(1000 + seq).toISOString(),
};
}
function makeTimelineEvent(
text: string,
type: string = "assistant_message",
@@ -97,6 +119,13 @@ function getUserTexts(items: StreamItem[]): string[] {
.map((item) => item.text);
}
function getAgentToolCalls(items: StreamItem[]) {
return items.filter(
(item): item is AgentToolCallItem =>
item.kind === "tool_call" && item.payload.source === "agent",
);
}
const baseTimelineInput: ProcessTimelineResponseInput = {
payload: {
agentId: "agent-1",
@@ -510,6 +539,68 @@ describe("processTimelineResponse", () => {
});
});
it("coalesces tool call lifecycle rows across the older-page prepend boundary", () => {
const callId = "toolu_boundary";
const currentTail = hydrateStreamState(
[
{
event: {
type: "timeline",
provider: "claude",
item: makeToolCallTimelineEntry(3, callId, "completed", {
type: "read",
filePath: "/tmp/example.ts",
}).item,
} as AgentStreamEventPayload,
timestamp: new Date(3000),
},
],
{ 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,
}),
],
},
});
const toolCalls = getAgentToolCalls(result.tail);
expect(
toolCalls.map((item) => ({
id: item.id,
callId: item.payload.data.callId,
status: item.payload.data.status,
detailType: item.payload.data.detail.type,
})),
).toEqual([
{
id: `agent_tool_${callId}`,
callId,
status: "completed",
detailType: "read",
},
]);
});
it("requests canonical catch-up when a projected entry overlaps unseen seqs", () => {
const existingCursor: TimelineCursor = {
epoch: "epoch-1",
@@ -3,7 +3,12 @@ import type { AgentLifecycleStatus } from "@server/shared/agent-lifecycle";
import type { Agent } from "@/stores/session-store";
import { useSessionStore } from "@/stores/session-store";
import type { StreamItem } from "@/types/stream";
import { applyStreamEvent, hydrateStreamState, reduceStreamUpdate } from "@/types/stream";
import {
applyStreamEvent,
coalesceAgentToolCallItems,
hydrateStreamState,
reduceStreamUpdate,
} from "@/types/stream";
const AGENT_STREAM_REDUCER_FLUSH_DELAY_MS = 16 * 3;
@@ -347,10 +352,10 @@ 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 [...olderTail, ...currentTail];
return coalesceAgentToolCallItems([...olderTail, ...currentTail]);
}
return [
return coalesceAgentToolCallItems([
...olderTail.slice(0, -1),
{
...olderLast,
@@ -358,7 +363,7 @@ function mergePrependedCanonicalTail(olderTail: StreamItem[], currentTail: Strea
timestamp: currentFirst.timestamp,
},
...currentTail.slice(1),
];
]);
}
function applyTimelineIncrementalPath(args: {