Skip to content

Commit 09e8447

Browse files
committed
Update: Optimize buildSteps for large event counts
1 parent c47d808 commit 09e8447

File tree

2 files changed

+65
-39
lines changed

2 files changed

+65
-39
lines changed

.changeset/optimize-build-steps.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@perstack/tui": patch
3+
---
4+
5+
Optimize buildSteps for large event counts using incremental updates
6+
7+
Instead of rebuilding the entire step map on every event addition, the step
8+
store now caches the map and only processes new events. Full rebuild only
9+
occurs when events are truncated (MAX_EVENTS exceeded) or historical events
10+
are loaded.

packages/tui/src/hooks/state/use-step-store.ts

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { RunEvent, ToolCall, ToolResult } from "@perstack/core"
2-
import { useCallback, useMemo, useState } from "react"
2+
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
33
import { UI_CONSTANTS } from "../../constants.js"
44
import type { DisplayStep, PerstackEvent, ToolExecution } from "../../types/index.js"
55

@@ -34,67 +34,83 @@ const extractQuery = (event: Extract<RunEvent, { type: "startRun" }>): string |
3434
if (userMessage?.type !== "userMessage") return undefined
3535
return userMessage.contents.find((c) => c.type === "textPart")?.text
3636
}
37-
const buildSteps = (events: PerstackEvent[]): DisplayStep[] => {
38-
const stepMap = new Map<number, StepBuilder>()
39-
const getOrCreateStep = (stepNumber: number): StepBuilder => {
40-
const existing = stepMap.get(stepNumber)
41-
if (existing) return existing
42-
const builder: StepBuilder = { tools: new Map() }
43-
stepMap.set(stepNumber, builder)
44-
return builder
45-
}
46-
for (const event of events) {
47-
if (!("stepNumber" in event)) continue
48-
const stepNum = event.stepNumber
49-
const builder = getOrCreateStep(stepNum)
50-
if (event.type === "startRun") {
51-
builder.query = extractQuery(event)
52-
} else if (event.type === "completeRun") {
53-
builder.completion = event.text
54-
} else if (isToolCallEvent(event)) {
55-
const { toolCall } = event
56-
builder.tools.set(toolCall.id, {
57-
id: toolCall.id,
58-
toolName: toolCall.toolName,
59-
args: toolCall.args as Record<string, unknown>,
60-
})
61-
} else if (isToolResultEvent(event)) {
62-
const { toolResult } = event
63-
const existing = builder.tools.get(toolResult.id)
64-
if (existing && Array.isArray(toolResult.result)) {
65-
existing.result = toolResult.result
66-
existing.isSuccess = checkIsSuccess(toolResult.result)
67-
}
37+
const getOrCreateStep = (stepMap: Map<number, StepBuilder>, stepNumber: number): StepBuilder => {
38+
const existing = stepMap.get(stepNumber)
39+
if (existing) return existing
40+
const builder: StepBuilder = { tools: new Map() }
41+
stepMap.set(stepNumber, builder)
42+
return builder
43+
}
44+
const processEvent = (stepMap: Map<number, StepBuilder>, event: PerstackEvent): void => {
45+
if (!("stepNumber" in event)) return
46+
const builder = getOrCreateStep(stepMap, event.stepNumber)
47+
if (event.type === "startRun") {
48+
builder.query = extractQuery(event)
49+
} else if (event.type === "completeRun") {
50+
builder.completion = event.text
51+
} else if (isToolCallEvent(event)) {
52+
const { toolCall } = event
53+
builder.tools.set(toolCall.id, {
54+
id: toolCall.id,
55+
toolName: toolCall.toolName,
56+
args: toolCall.args as Record<string, unknown>,
57+
})
58+
} else if (isToolResultEvent(event)) {
59+
const { toolResult } = event
60+
const existing = builder.tools.get(toolResult.id)
61+
if (existing && Array.isArray(toolResult.result)) {
62+
existing.result = toolResult.result
63+
existing.isSuccess = checkIsSuccess(toolResult.result)
6864
}
6965
}
70-
return Array.from(stepMap.entries())
66+
}
67+
const buildStepsFromMap = (stepMap: Map<number, StepBuilder>): DisplayStep[] =>
68+
Array.from(stepMap.entries())
7169
.sort(([a], [b]) => a - b)
7270
.map(([stepNumber, builder]) => ({
7371
id: `step-${stepNumber}`,
7472
stepNumber,
7573
query: builder.query,
76-
tools: Array.from(builder.tools.values()),
74+
tools: Array.from(builder.tools.values()).map((tool) => ({ ...tool })),
7775
completion: builder.completion,
7876
}))
79-
}
8077
export const useStepStore = () => {
8178
const [events, setEvents] = useState<PerstackEvent[]>([])
79+
const [steps, setSteps] = useState<DisplayStep[]>([])
80+
const stepMapRef = useRef<Map<number, StepBuilder>>(new Map())
81+
const processedCountRef = useRef(0)
82+
const needsRebuildRef = useRef(false)
8283
const addEvent = useCallback((event: PerstackEvent) => {
8384
setEvents((prev) => {
8485
const newEvents = [...prev, event]
85-
return newEvents.length > UI_CONSTANTS.MAX_EVENTS
86-
? newEvents.slice(-UI_CONSTANTS.MAX_EVENTS)
87-
: newEvents
86+
if (newEvents.length > UI_CONSTANTS.MAX_EVENTS) {
87+
needsRebuildRef.current = true
88+
return newEvents.slice(-UI_CONSTANTS.MAX_EVENTS)
89+
}
90+
return newEvents
8891
})
8992
}, [])
9093
const setHistoricalEvents = useCallback((historicalEvents: PerstackEvent[]) => {
94+
needsRebuildRef.current = true
9195
setEvents(
9296
historicalEvents.length > UI_CONSTANTS.MAX_EVENTS
9397
? historicalEvents.slice(-UI_CONSTANTS.MAX_EVENTS)
9498
: historicalEvents,
9599
)
96100
}, [])
97-
const steps = useMemo(() => buildSteps(events), [events])
101+
useEffect(() => {
102+
if (needsRebuildRef.current) {
103+
stepMapRef.current = new Map()
104+
processedCountRef.current = 0
105+
needsRebuildRef.current = false
106+
}
107+
const newEvents = events.slice(processedCountRef.current)
108+
for (const event of newEvents) {
109+
processEvent(stepMapRef.current, event)
110+
}
111+
processedCountRef.current = events.length
112+
setSteps(buildStepsFromMap(stepMapRef.current))
113+
}, [events])
98114
const completedSteps = useMemo(() => {
99115
if (steps.length === 0) return []
100116
const lastStep = steps.at(-1)

0 commit comments

Comments
 (0)