126 lines
4.3 KiB
TypeScript
126 lines
4.3 KiB
TypeScript
import type { ToolSchema } from '@deepseek-ai/dsh-llm/types'
|
|
import type { HistoryEntry } from '@deepseek-ai/dsh-client-connection/client'
|
|
import type {
|
|
CodeSubCall, ConversationNode, PartialAssistant, RunningToolCall,
|
|
} from './conversation.ts'
|
|
import type { ConversationContext } from './conversation-context.ts'
|
|
import { projectConversationHistory } from '../session-history/history-fold.ts'
|
|
import { inspectRequests, type RequestView } from './request-inspection.ts'
|
|
|
|
function assistantStepKey(turn: number, step: number): string {
|
|
return `${turn}\u0000${step}`
|
|
}
|
|
|
|
function isFirstTokenCandidate(entry: HistoryEntry): boolean {
|
|
const event = entry.event
|
|
if (event.type !== 'assistant/chunk') return false
|
|
switch (event.data.chunk.type) {
|
|
case 'text-delta':
|
|
case 'reasoning-delta':
|
|
return event.data.chunk.text !== ''
|
|
case 'tool-call-delta':
|
|
return event.data.chunk.argumentsDelta !== '' || event.data.chunk.name !== undefined
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
/** Lazily derived inspection data for one immutable session-history window. */
|
|
export interface SessionHistoryInspection {
|
|
eventNodes: readonly ConversationNode[]
|
|
contexts: readonly ConversationContext[]
|
|
requests: readonly RequestView[]
|
|
callSchemas: ReadonlyMap<string, ToolSchema>
|
|
interruptedNodes: readonly ConversationNode[]
|
|
partial: PartialAssistant | null
|
|
runningCalls: readonly RunningToolCall[]
|
|
codeDispatches: ReadonlyMap<string, readonly CodeSubCall[]>
|
|
}
|
|
|
|
/**
|
|
* Remove completed-step token payloads that no inspection projection reads.
|
|
* The first visible token preserves timing, usage chunks preserve accounting,
|
|
* and unfinished steps retain every chunk for live or interrupted content.
|
|
* @param entries - Contiguous raw history entries in sequence order.
|
|
* @returns A projection-equivalent, usually much smaller entry ledger.
|
|
*/
|
|
export function compactHistoryInspectionEntries(
|
|
entries: readonly HistoryEntry[],
|
|
): readonly HistoryEntry[] {
|
|
const completedSteps = new Set<string>()
|
|
for (const { event } of entries) {
|
|
if (event.type === 'assistant/message') {
|
|
completedSteps.add(assistantStepKey(event.data.turn, event.data.step))
|
|
}
|
|
}
|
|
|
|
const firstTokenSteps = new Set<string>()
|
|
const compacted: HistoryEntry[] = []
|
|
let changed = false
|
|
for (const entry of entries) {
|
|
const event = entry.event
|
|
if (event.type !== 'assistant/chunk') {
|
|
compacted.push(entry)
|
|
continue
|
|
}
|
|
const key = assistantStepKey(event.data.turn, event.data.step)
|
|
if (!completedSteps.has(key) || event.data.chunk.type === 'usage') {
|
|
compacted.push(entry)
|
|
continue
|
|
}
|
|
if (isFirstTokenCandidate(entry) && !firstTokenSteps.has(key)) {
|
|
firstTokenSteps.add(key)
|
|
compacted.push(entry)
|
|
} else {
|
|
changed = true
|
|
}
|
|
}
|
|
return changed ? compacted : entries
|
|
}
|
|
|
|
/**
|
|
* Create a lazy inspection projection over an immutable history window.
|
|
* Conversation consumers retain the cheap wrapper; only Trajectory snapshots
|
|
* the entries and replays event order and request lifecycle state.
|
|
* @param loadEntries - Lazily snapshots contiguous raw entries in sequence order.
|
|
* @returns Lazy, memoized inspection fields for that exact window.
|
|
*/
|
|
export function createHistoryInspection(
|
|
loadEntries: () => readonly HistoryEntry[],
|
|
): SessionHistoryInspection {
|
|
let entries: readonly HistoryEntry[] | undefined
|
|
let conversation: ReturnType<typeof projectConversationHistory> | undefined
|
|
let requests: ReturnType<typeof inspectRequests> | undefined
|
|
const historyEntries = () => entries ??= loadEntries()
|
|
const conversationProjection = () =>
|
|
conversation ??= projectConversationHistory(historyEntries())
|
|
const requestProjection = () =>
|
|
requests ??= inspectRequests(historyEntries())
|
|
return {
|
|
get eventNodes() {
|
|
return conversationProjection().eventNodes
|
|
},
|
|
get contexts() {
|
|
return conversationProjection().contexts
|
|
},
|
|
get interruptedNodes() {
|
|
return conversationProjection().interruptedNodes
|
|
},
|
|
get partial() {
|
|
return conversationProjection().partial
|
|
},
|
|
get runningCalls() {
|
|
return conversationProjection().runningCalls
|
|
},
|
|
get codeDispatches() {
|
|
return conversationProjection().codeDispatches
|
|
},
|
|
get requests() {
|
|
return requestProjection().requests
|
|
},
|
|
get callSchemas() {
|
|
return requestProjection().callSchemas
|
|
},
|
|
}
|
|
}
|