mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix app stream head replacement during init hydration (#663)
This commit is contained in:
@@ -910,6 +910,67 @@ describe("processAgentStreamEvents", () => {
|
|||||||
lastActivityAt: new Date(3000),
|
lastActivityAt: new Date(3000),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps a live Claude assistant paragraph contiguous when init tail hydration lands mid-stream", () => {
|
||||||
|
const seq186Text = "Now let me write the updated tool-call-detail-parser.ts with all the sub";
|
||||||
|
const seq187Text = "agent additions.";
|
||||||
|
|
||||||
|
const liveSeq186 = processAgentStreamEvent({
|
||||||
|
...baseStreamInput,
|
||||||
|
event: makeTimelineEvent(seq186Text),
|
||||||
|
seq: 186,
|
||||||
|
epoch: "epoch-1",
|
||||||
|
currentTail: [],
|
||||||
|
currentHead: [],
|
||||||
|
currentCursor: undefined,
|
||||||
|
timestamp: new Date("2026-05-02T10:00:00.186Z"),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getAssistantTexts(liveSeq186.head)).toEqual([seq186Text]);
|
||||||
|
expect(getAssistantTexts(liveSeq186.tail)).toEqual([]);
|
||||||
|
|
||||||
|
const initTailHydration = processTimelineResponse({
|
||||||
|
...baseTimelineInput,
|
||||||
|
currentTail: liveSeq186.tail,
|
||||||
|
currentHead: liveSeq186.head,
|
||||||
|
currentCursor: liveSeq186.cursor ?? undefined,
|
||||||
|
isInitializing: true,
|
||||||
|
hasActiveInitDeferred: true,
|
||||||
|
initRequestDirection: "tail",
|
||||||
|
payload: {
|
||||||
|
agentId: "agent-1",
|
||||||
|
direction: "tail",
|
||||||
|
reset: false,
|
||||||
|
epoch: "epoch-1",
|
||||||
|
startCursor: { seq: 186 },
|
||||||
|
endCursor: { seq: 186 },
|
||||||
|
entries: [makeTimelineEntry(186, seq186Text)],
|
||||||
|
error: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getAssistantTexts(initTailHydration.tail)).toEqual([]);
|
||||||
|
expect(getAssistantTexts(initTailHydration.head)).toEqual([seq186Text]);
|
||||||
|
|
||||||
|
const liveSeq187 = processAgentStreamEvent({
|
||||||
|
...baseStreamInput,
|
||||||
|
event: makeTimelineEvent(seq187Text),
|
||||||
|
seq: 187,
|
||||||
|
epoch: "epoch-1",
|
||||||
|
currentTail: initTailHydration.tail,
|
||||||
|
currentHead: initTailHydration.head,
|
||||||
|
currentCursor: initTailHydration.cursor ?? undefined,
|
||||||
|
timestamp: new Date("2026-05-02T10:00:00.187Z"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const finalAssistantItems = [...liveSeq187.tail, ...liveSeq187.head].filter(
|
||||||
|
(item): item is Extract<StreamItem, { kind: "assistant_message" }> =>
|
||||||
|
item.kind === "assistant_message",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(finalAssistantItems).toHaveLength(1);
|
||||||
|
expect(finalAssistantItems[0]?.text).toBe(`${seq186Text}${seq187Text}`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createAgentStreamReducerQueue", () => {
|
describe("createAgentStreamReducerQueue", () => {
|
||||||
|
|||||||
@@ -99,16 +99,49 @@ interface TimelinePathResult {
|
|||||||
sideEffects: TimelineReducerSideEffect[];
|
sideEffects: TimelineReducerSideEffect[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function preserveReplacePathAssistantHead(params: {
|
||||||
|
tail: StreamItem[];
|
||||||
|
currentHead: StreamItem[];
|
||||||
|
}): {
|
||||||
|
tail: StreamItem[];
|
||||||
|
head: StreamItem[];
|
||||||
|
} {
|
||||||
|
const { tail, currentHead } = params;
|
||||||
|
const liveAssistant = currentHead.findLast(
|
||||||
|
(item): item is Extract<StreamItem, { kind: "assistant_message" }> =>
|
||||||
|
item.kind === "assistant_message",
|
||||||
|
);
|
||||||
|
if (!liveAssistant) {
|
||||||
|
return { tail, head: [] };
|
||||||
|
}
|
||||||
|
const tailAssistant = tail.at(-1);
|
||||||
|
if (!tailAssistant || tailAssistant.kind !== "assistant_message") {
|
||||||
|
return { tail, head: currentHead };
|
||||||
|
}
|
||||||
|
if (!liveAssistant.text.startsWith(tailAssistant.text)) {
|
||||||
|
return { tail, head: [] };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tail: tail.slice(0, -1),
|
||||||
|
head: [{ ...liveAssistant, text: tailAssistant.text }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function applyTimelineReplacePath(args: {
|
function applyTimelineReplacePath(args: {
|
||||||
timelineUnits: TimelineUnit[];
|
timelineUnits: TimelineUnit[];
|
||||||
payload: ProcessTimelineResponseInput["payload"];
|
payload: ProcessTimelineResponseInput["payload"];
|
||||||
bootstrapPolicy: ReturnType<typeof deriveBootstrapTailTimelinePolicy>;
|
bootstrapPolicy: ReturnType<typeof deriveBootstrapTailTimelinePolicy>;
|
||||||
|
currentHead: StreamItem[];
|
||||||
toHydratedEvents: (
|
toHydratedEvents: (
|
||||||
units: TimelineUnit[],
|
units: TimelineUnit[],
|
||||||
) => Array<{ event: AgentStreamEventPayload; timestamp: Date }>;
|
) => Array<{ event: AgentStreamEventPayload; timestamp: Date }>;
|
||||||
}): TimelinePathResult {
|
}): TimelinePathResult {
|
||||||
const { timelineUnits, payload, bootstrapPolicy, toHydratedEvents } = args;
|
const { timelineUnits, payload, bootstrapPolicy, currentHead, toHydratedEvents } = args;
|
||||||
const tail = hydrateStreamState(toHydratedEvents(timelineUnits), { source: "canonical" });
|
const hydratedTail = hydrateStreamState(toHydratedEvents(timelineUnits), { source: "canonical" });
|
||||||
|
const { tail, head } = preserveReplacePathAssistantHead({
|
||||||
|
tail: hydratedTail,
|
||||||
|
currentHead,
|
||||||
|
});
|
||||||
const cursor: TimelineCursor | null =
|
const cursor: TimelineCursor | null =
|
||||||
payload.startCursor && payload.endCursor
|
payload.startCursor && payload.endCursor
|
||||||
? {
|
? {
|
||||||
@@ -121,7 +154,7 @@ function applyTimelineReplacePath(args: {
|
|||||||
if (bootstrapPolicy.catchUpCursor) {
|
if (bootstrapPolicy.catchUpCursor) {
|
||||||
sideEffects.push({ type: "catch_up", cursor: bootstrapPolicy.catchUpCursor });
|
sideEffects.push({ type: "catch_up", cursor: bootstrapPolicy.catchUpCursor });
|
||||||
}
|
}
|
||||||
return { tail, head: [], cursor, cursorChanged: true, sideEffects };
|
return { tail, head, cursor, cursorChanged: true, sideEffects };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IncrementalAcceptResult {
|
interface IncrementalAcceptResult {
|
||||||
@@ -307,6 +340,7 @@ export function processTimelineResponse(
|
|||||||
timelineUnits,
|
timelineUnits,
|
||||||
payload,
|
payload,
|
||||||
bootstrapPolicy,
|
bootstrapPolicy,
|
||||||
|
currentHead,
|
||||||
toHydratedEvents,
|
toHydratedEvents,
|
||||||
})
|
})
|
||||||
: applyTimelineIncrementalPath({
|
: applyTimelineIncrementalPath({
|
||||||
|
|||||||
Reference in New Issue
Block a user