Optimize agent stream rendering

This commit is contained in:
Mohamed Boudra
2026-04-22 15:38:53 +07:00
parent 3c5bfd6e3d
commit aef54d5180
7 changed files with 1009 additions and 308 deletions
+12 -85
View File
@@ -8,8 +8,8 @@ import { clearArchiveAgentPending } from "@/hooks/use-archive-agent";
import { prefetchProvidersSnapshot } from "@/hooks/use-providers-snapshot";
import { generateMessageId, type StreamItem } from "@/types/stream";
import {
createSessionAgentStreamReducerQueue,
processTimelineResponse,
processAgentStreamEvent,
} from "@/contexts/session-stream-reducers";
import type {
ActivityLogPayload,
@@ -1049,6 +1049,14 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
applyAgentUpdatePayload(update);
});
const agentStreamReducerQueue = createSessionAgentStreamReducerQueue({
serverId,
setAgentStreamState,
setAgentTimelineCursor,
setAgents,
requestCanonicalCatchUp,
});
const unsubAgentStream = client.on("agent_stream", (message) => {
if (message.type !== "agent_stream") return;
const { agentId, event, timestamp, seq, epoch } = message.payload;
@@ -1075,96 +1083,13 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
}
}
// Read current store state
const session = useSessionStore.getState().sessions[serverId];
const currentTail = session?.agentStreamTail.get(agentId) ?? [];
const currentHead = session?.agentStreamHead.get(agentId) ?? [];
const currentCursor = session?.agentTimelineCursor.get(agentId);
const currentAgentEntry = session?.agents.get(agentId);
const currentAgent = currentAgentEntry
? {
status: currentAgentEntry.status,
updatedAt: currentAgentEntry.updatedAt,
lastActivityAt: currentAgentEntry.lastActivityAt,
}
: null;
// Call pure reducer
const result = processAgentStreamEvent({
agentStreamReducerQueue.enqueue(agentId, {
event: streamEvent,
seq,
epoch,
currentTail,
currentHead,
currentCursor,
currentAgent,
timestamp: parsedTimestamp,
});
// Apply tail/head patches
if (result.changedTail || result.changedHead) {
setAgentStreamState(serverId, agentId, {
...(result.changedTail ? { tail: result.tail } : {}),
...(result.changedHead ? { head: result.head } : {}),
});
}
// Apply cursor patch
if (result.cursorChanged && result.cursor) {
const nextCursor = result.cursor;
setAgentTimelineCursor(serverId, (prev) => {
const current = prev.get(agentId);
if (
current &&
typeof seq === "number" &&
typeof epoch === "string" &&
current.epoch === epoch &&
seq >= current.startSeq &&
seq <= current.endSeq
) {
// Fast-path: seq stays inside the current range during streaming.
return prev;
}
if (
current &&
current.epoch === nextCursor.epoch &&
current.startSeq === nextCursor.startSeq &&
current.endSeq === nextCursor.endSeq
) {
return prev;
}
const next = new Map(prev);
next.set(agentId, nextCursor);
return next;
});
}
// Apply agent patch (optimistic lifecycle)
if (result.agentChanged && result.agent) {
const nextAgent = result.agent;
setAgents(serverId, (prev) => {
const current = prev.get(agentId);
if (!current) {
return prev;
}
const next = new Map(prev);
next.set(agentId, {
...current,
status: nextAgent.status,
updatedAt: nextAgent.updatedAt,
lastActivityAt: nextAgent.lastActivityAt,
});
return next;
});
}
// Execute side effects
for (const effect of result.sideEffects) {
if (effect.type === "catch_up") {
requestCanonicalCatchUp(agentId, effect.cursor);
}
}
// NOTE: We don't update lastActivityAt on every stream event to prevent
// cascading rerenders. The agent_update handler updates agent.lastActivityAt
// on status changes, which is sufficient for sorting and display purposes.
@@ -1172,6 +1097,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
const unsubAgentTimeline = client.on("fetch_agent_timeline_response", (message) => {
if (message.type !== "fetch_agent_timeline_response") return;
agentStreamReducerQueue.flushAgent(message.payload.agentId);
applyTimelineResponse(message.payload);
});
@@ -1572,6 +1498,7 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
unsubVoiceInputState();
unsubAgentDeleted();
unsubAgentArchived();
agentStreamReducerQueue.dispose({ flush: true });
};
}, [
client,
@@ -2,10 +2,13 @@ import { describe, expect, it } from "vitest";
import type { AgentStreamEventPayload } from "@server/shared/messages";
import type { StreamItem } from "@/types/stream";
import {
createAgentStreamReducerQueue,
processTimelineResponse,
processAgentStreamEvent,
processAgentStreamEvents,
type ProcessTimelineResponseInput,
type ProcessAgentStreamEventInput,
type AgentStreamReducerEvent,
type TimelineCursor,
} from "./session-stream-reducers";
@@ -41,6 +44,36 @@ function makeUserTimelineEvent(text: string): AgentStreamEventPayload {
} as AgentStreamEventPayload;
}
function makeToolCallTimelineEvent(callId: string): AgentStreamEventPayload {
return {
type: "timeline",
provider: "claude",
item: {
type: "tool_call",
callId,
name: "Read",
status: "running",
detail: {
type: "read",
filePath: "/tmp/example.ts",
},
error: null,
},
} as AgentStreamEventPayload;
}
function makeStreamReducerEvent(
event: AgentStreamEventPayload,
seq: number,
): AgentStreamReducerEvent {
return {
event,
seq,
epoch: "epoch-1",
timestamp: new Date(1000 + seq),
};
}
const baseTimelineInput: ProcessTimelineResponseInput = {
payload: {
agentId: "agent-1",
@@ -647,3 +680,270 @@ describe("processAgentStreamEvent", () => {
expect(result.agent).toBe(null);
});
});
describe("processAgentStreamEvents", () => {
it("coalesces contiguous assistant stream events into one head update and final cursor", () => {
const result = processAgentStreamEvents({
events: [
makeStreamReducerEvent(makeTimelineEvent("Hello"), 1),
makeStreamReducerEvent(makeTimelineEvent(" world"), 2),
],
currentTail: [],
currentHead: [],
currentCursor: undefined,
currentAgent: null,
});
expect(result.changedTail).toBe(false);
expect(result.changedHead).toBe(true);
expect(result.tail).toEqual([]);
expect(result.head).toHaveLength(1);
expect(result.head[0]).toMatchObject({
kind: "assistant_message",
text: "Hello world",
});
expect(result.cursor).toEqual({
epoch: "epoch-1",
startSeq: 1,
endSeq: 2,
} satisfies TimelineCursor);
expect(result.sideEffects).toEqual([]);
});
it("promotes completed assistant markdown blocks to tail while keeping the live block in head", () => {
const result = processAgentStreamEvents({
events: [
makeStreamReducerEvent(makeTimelineEvent("First paragraph"), 1),
makeStreamReducerEvent(makeTimelineEvent("\n\nSecond paragraph"), 2),
],
currentTail: [],
currentHead: [],
currentCursor: undefined,
currentAgent: null,
});
expect(result.changedTail).toBe(true);
expect(result.changedHead).toBe(true);
expect(result.tail).toHaveLength(1);
expect(result.tail[0]).toMatchObject({
kind: "assistant_message",
text: "First paragraph",
});
expect(result.head).toHaveLength(1);
expect(result.head[0]).toMatchObject({
kind: "assistant_message",
text: "Second paragraph",
});
});
it("does not promote a markdown block that is still inside an open code fence", () => {
const result = processAgentStreamEvents({
events: [
makeStreamReducerEvent(makeTimelineEvent("Before fence\n\n```ts\nconst a = 1;"), 1),
makeStreamReducerEvent(makeTimelineEvent("\n\nconst b = 2;"), 2),
],
currentTail: [],
currentHead: [],
currentCursor: undefined,
currentAgent: null,
});
expect(result.changedTail).toBe(true);
expect(result.tail).toHaveLength(1);
expect(result.tail[0]).toMatchObject({
kind: "assistant_message",
text: "Before fence",
});
expect(result.head).toHaveLength(1);
expect(result.head[0]).toMatchObject({
kind: "assistant_message",
text: "```ts\nconst a = 1;\n\nconst b = 2;",
});
});
it("flushes the live assistant block before applying a tool call in the same reducer pass", () => {
const result = processAgentStreamEvents({
events: [
makeStreamReducerEvent(makeTimelineEvent("Before tool"), 1),
makeStreamReducerEvent(makeToolCallTimelineEvent("call-1"), 2),
],
currentTail: [],
currentHead: [],
currentCursor: undefined,
currentAgent: null,
});
expect(result.changedTail).toBe(true);
expect(result.changedHead).toBe(true);
expect(result.head).toEqual([]);
expect(result.tail.map((item: StreamItem) => item.kind)).toEqual([
"assistant_message",
"tool_call",
]);
expect(result.cursor).toEqual({
epoch: "epoch-1",
startSeq: 1,
endSeq: 2,
} satisfies TimelineCursor);
});
it("returns the final optimistic lifecycle patch across a batch", () => {
const result = processAgentStreamEvents({
events: [
makeStreamReducerEvent(makeTimelineEvent("Done"), 1),
{
event: { type: "turn_completed", provider: "claude" } as AgentStreamEventPayload,
seq: undefined,
epoch: undefined,
timestamp: new Date(3000),
},
],
currentTail: [],
currentHead: [],
currentCursor: undefined,
currentAgent: {
status: "running",
updatedAt: new Date(1000),
lastActivityAt: new Date(1000),
},
});
expect(result.head).toEqual([]);
expect(result.tail).toHaveLength(1);
expect(result.agentChanged).toBe(true);
expect(result.agent).toMatchObject({
status: "idle",
updatedAt: new Date(3000),
lastActivityAt: new Date(3000),
});
});
});
describe("createAgentStreamReducerQueue", () => {
function createManualScheduler() {
let nextId = 1;
const callbacks = new Map<number, () => void>();
return {
schedule(callback: () => void) {
const id = nextId;
nextId += 1;
callbacks.set(id, callback);
return id;
},
cancel(id: number) {
callbacks.delete(id);
},
flushOne() {
const entry = callbacks.entries().next().value;
if (!entry) {
throw new Error("Expected a scheduled callback");
}
const [id, callback] = entry;
callbacks.delete(id);
callback();
},
get size() {
return callbacks.size;
},
};
}
it("coalesces multiple events for one agent into one scheduled commit", () => {
const scheduler = createManualScheduler();
const commits: Array<{ agentId: string; headText: string; cursorEndSeq: number | null }> = [];
let currentTail: StreamItem[] = [];
let currentHead: StreamItem[] = [];
const queue = createAgentStreamReducerQueue({
getSnapshot: () => ({
currentTail,
currentHead,
currentCursor: undefined,
currentAgent: null,
}),
commit: (agentId, result) => {
currentTail = result.tail;
currentHead = result.head;
commits.push({
agentId,
headText: result.head[0]?.kind === "assistant_message" ? result.head[0].text : "",
cursorEndSeq: result.cursor?.endSeq ?? null,
});
},
handleSideEffects: () => {},
scheduleFlush: scheduler.schedule,
cancelFlush: scheduler.cancel,
});
queue.enqueue("agent-1", makeStreamReducerEvent(makeTimelineEvent("Hello"), 1));
queue.enqueue("agent-1", makeStreamReducerEvent(makeTimelineEvent(" world"), 2));
expect(scheduler.size).toBe(1);
expect(commits).toEqual([]);
scheduler.flushOne();
expect(commits).toEqual([
{
agentId: "agent-1",
headText: "Hello world",
cursorEndSeq: 2,
},
]);
expect(scheduler.size).toBe(0);
});
it("flushes queued events synchronously for one agent before canonical history is applied", () => {
const scheduler = createManualScheduler();
const commits: string[] = [];
const queue = createAgentStreamReducerQueue({
getSnapshot: () => ({
currentTail: [],
currentHead: [],
currentCursor: undefined,
currentAgent: null,
}),
commit: (agentId, result) => {
commits.push(
`${agentId}:${result.head[0]?.kind === "assistant_message" ? result.head[0].text : ""}`,
);
},
handleSideEffects: () => {},
scheduleFlush: scheduler.schedule,
cancelFlush: scheduler.cancel,
});
queue.enqueue("agent-1", makeStreamReducerEvent(makeTimelineEvent("queued"), 1));
queue.flushAgent("agent-1");
expect(commits).toEqual(["agent-1:queued"]);
expect(scheduler.size).toBe(0);
});
it("flushes queued events synchronously before disposal", () => {
const scheduler = createManualScheduler();
const commits: string[] = [];
const queue = createAgentStreamReducerQueue({
getSnapshot: () => ({
currentTail: [],
currentHead: [],
currentCursor: undefined,
currentAgent: null,
}),
commit: (agentId, result) => {
commits.push(
`${agentId}:${result.head[0]?.kind === "assistant_message" ? result.head[0].text : ""}`,
);
},
handleSideEffects: () => {},
scheduleFlush: scheduler.schedule,
cancelFlush: scheduler.cancel,
});
queue.enqueue("agent-1", makeStreamReducerEvent(makeTimelineEvent("queued"), 1));
queue.dispose({ flush: true });
expect(commits).toEqual(["agent-1:queued"]);
expect(scheduler.size).toBe(0);
});
});
@@ -1,5 +1,7 @@
import type { AgentStreamEventPayload } from "@server/shared/messages";
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,
@@ -17,6 +19,8 @@ import {
} from "@/contexts/session-timeline-bootstrap-policy";
import { deriveOptimisticLifecycleStatus } from "@/contexts/session-stream-lifecycle";
const AGENT_STREAM_REDUCER_FLUSH_DELAY_MS = 16 * 3;
// ---------------------------------------------------------------------------
// Shared cursor type
// ---------------------------------------------------------------------------
@@ -324,6 +328,62 @@ export interface ProcessAgentStreamEventOutput {
sideEffects: AgentStreamReducerSideEffect[];
}
export type AgentStreamReducerEvent = {
event: AgentStreamEventPayload;
seq: number | undefined;
epoch: string | undefined;
timestamp: Date;
};
export type AgentStreamReducerAgentSnapshot = {
status: AgentLifecycleStatus;
updatedAt: Date;
lastActivityAt: Date;
};
export type ProcessAgentStreamEventsInput = {
events: AgentStreamReducerEvent[];
currentTail: StreamItem[];
currentHead: StreamItem[];
currentCursor: TimelineCursor | undefined;
currentAgent: AgentStreamReducerAgentSnapshot | null;
};
export type AgentStreamReducerSnapshot = Omit<ProcessAgentStreamEventsInput, "events">;
export type AgentStreamReducerQueue = {
enqueue: (agentId: string, event: AgentStreamReducerEvent) => void;
flush: () => void;
flushAgent: (agentId: string) => void;
dispose: (options?: { flush?: boolean }) => void;
};
export type CreateAgentStreamReducerQueueInput = {
getSnapshot: (agentId: string) => AgentStreamReducerSnapshot;
commit: (
agentId: string,
result: ProcessAgentStreamEventOutput,
events: AgentStreamReducerEvent[],
) => void;
handleSideEffects: (agentId: string, sideEffects: AgentStreamReducerSideEffect[]) => void;
scheduleFlush: (callback: () => void) => number;
cancelFlush: (id: number) => void;
};
function applyAgentPatch(
currentAgent: AgentStreamReducerAgentSnapshot | null,
patch: AgentPatch | null,
): AgentStreamReducerAgentSnapshot | null {
if (!currentAgent || !patch) {
return currentAgent;
}
return {
status: patch.status,
updatedAt: patch.updatedAt,
lastActivityAt: patch.lastActivityAt,
};
}
export function processAgentStreamEvent(
input: ProcessAgentStreamEventInput,
): ProcessAgentStreamEventOutput {
@@ -430,3 +490,255 @@ export function processAgentStreamEvent(
sideEffects,
};
}
export function processAgentStreamEvents(
input: ProcessAgentStreamEventsInput,
): ProcessAgentStreamEventOutput {
let tail = input.currentTail;
let head = input.currentHead;
let cursor = input.currentCursor;
let agent = input.currentAgent;
let changedTail = false;
let changedHead = false;
let cursorChanged = false;
let agentPatch: AgentPatch | null = null;
let agentChanged = false;
const sideEffects: AgentStreamReducerSideEffect[] = [];
for (const reducerEvent of input.events) {
const result = processAgentStreamEvent({
event: reducerEvent.event,
seq: reducerEvent.seq,
epoch: reducerEvent.epoch,
currentTail: tail,
currentHead: head,
currentCursor: cursor,
currentAgent: agent,
timestamp: reducerEvent.timestamp,
});
tail = result.tail;
head = result.head;
changedTail = changedTail || result.changedTail;
changedHead = changedHead || result.changedHead;
sideEffects.push(...result.sideEffects);
if (result.cursorChanged) {
cursor = result.cursor ?? undefined;
cursorChanged = true;
}
if (result.agentChanged) {
agentPatch = result.agent;
agentChanged = true;
agent = applyAgentPatch(agent, result.agent);
}
}
return {
tail,
head,
changedTail,
changedHead,
cursor: cursor ?? null,
cursorChanged,
agent: agentPatch,
agentChanged,
sideEffects,
};
}
export function createAgentStreamReducerQueue(
input: CreateAgentStreamReducerQueueInput,
): AgentStreamReducerQueue {
const pendingByAgentId = new Map<string, AgentStreamReducerEvent[]>();
let scheduledFlushId: number | null = null;
const cancelScheduledFlush = () => {
if (scheduledFlushId === null) {
return;
}
input.cancelFlush(scheduledFlushId);
scheduledFlushId = null;
};
const flushAgent = (agentId: string) => {
const events = pendingByAgentId.get(agentId);
if (!events || events.length === 0) {
return;
}
pendingByAgentId.delete(agentId);
if (pendingByAgentId.size === 0) {
cancelScheduledFlush();
}
const result = processAgentStreamEvents({
events,
...input.getSnapshot(agentId),
});
input.commit(agentId, result, events);
if (result.sideEffects.length > 0) {
input.handleSideEffects(agentId, result.sideEffects);
}
};
const flush = () => {
const agentIds = Array.from(pendingByAgentId.keys());
for (const agentId of agentIds) {
flushAgent(agentId);
}
};
const scheduleFlush = () => {
if (scheduledFlushId !== null) {
return;
}
scheduledFlushId = input.scheduleFlush(() => {
scheduledFlushId = null;
flush();
});
};
return {
enqueue(agentId, event) {
const pending = pendingByAgentId.get(agentId);
if (pending) {
pending.push(event);
} else {
pendingByAgentId.set(agentId, [event]);
}
scheduleFlush();
},
flush,
flushAgent,
dispose(options) {
cancelScheduledFlush();
if (options?.flush) {
flush();
} else {
pendingByAgentId.clear();
}
},
};
}
type StreamStatePatch = {
tail?: StreamItem[];
head?: StreamItem[];
};
export type CreateSessionAgentStreamReducerQueueInput = {
serverId: string;
setAgentStreamState: (serverId: string, agentId: string, state: StreamStatePatch) => void;
setAgentTimelineCursor: (
serverId: string,
state: (prev: Map<string, TimelineCursor>) => Map<string, TimelineCursor>,
) => void;
setAgents: (serverId: string, state: (prev: Map<string, Agent>) => Map<string, Agent>) => void;
requestCanonicalCatchUp: (agentId: string, cursor: { epoch: string; endSeq: number }) => void;
};
function scheduleAgentStreamReducerFlush(callback: () => void): number {
return setTimeout(callback, AGENT_STREAM_REDUCER_FLUSH_DELAY_MS) as unknown as number;
}
function cancelAgentStreamReducerFlush(id: number) {
clearTimeout(id);
}
export function createSessionAgentStreamReducerQueue(
input: CreateSessionAgentStreamReducerQueueInput,
): AgentStreamReducerQueue {
const {
serverId,
setAgentStreamState,
setAgentTimelineCursor,
setAgents,
requestCanonicalCatchUp,
} = input;
return createAgentStreamReducerQueue({
getSnapshot: (agentId) => {
const session = useSessionStore.getState().sessions[serverId];
const currentAgentEntry = session?.agents.get(agentId);
return {
currentTail: session?.agentStreamTail.get(agentId) ?? [],
currentHead: session?.agentStreamHead.get(agentId) ?? [],
currentCursor: session?.agentTimelineCursor.get(agentId),
currentAgent: currentAgentEntry
? {
status: currentAgentEntry.status,
updatedAt: currentAgentEntry.updatedAt,
lastActivityAt: currentAgentEntry.lastActivityAt,
}
: null,
};
},
commit: (agentId, result, events) => {
if (result.changedTail || result.changedHead) {
setAgentStreamState(serverId, agentId, {
...(result.changedTail ? { tail: result.tail } : {}),
...(result.changedHead ? { head: result.head } : {}),
});
}
if (result.cursorChanged && result.cursor) {
const nextCursor = result.cursor;
const lastEvent = events.at(-1);
setAgentTimelineCursor(serverId, (prev) => {
const current = prev.get(agentId);
if (
current &&
lastEvent &&
typeof lastEvent.seq === "number" &&
typeof lastEvent.epoch === "string" &&
current.epoch === lastEvent.epoch &&
lastEvent.seq >= current.startSeq &&
lastEvent.seq <= current.endSeq
) {
return prev;
}
if (
current &&
current.epoch === nextCursor.epoch &&
current.startSeq === nextCursor.startSeq &&
current.endSeq === nextCursor.endSeq
) {
return prev;
}
const next = new Map(prev);
next.set(agentId, nextCursor);
return next;
});
}
if (result.agentChanged && result.agent) {
const nextAgent = result.agent;
setAgents(serverId, (prev) => {
const current = prev.get(agentId);
if (!current) {
return prev;
}
const next = new Map(prev);
next.set(agentId, {
...current,
status: nextAgent.status,
updatedAt: nextAgent.updatedAt,
lastActivityAt: nextAgent.lastActivityAt,
});
return next;
});
}
},
handleSideEffects: (agentId, sideEffects) => {
for (const effect of sideEffects) {
if (effect.type === "catch_up") {
requestCanonicalCatchUp(agentId, effect.cursor);
}
}
},
scheduleFlush: scheduleAgentStreamReducerFlush,
cancelFlush: cancelAgentStreamReducerFlush,
});
}