1
/** Stateful reconstruction of historical tool definitions for request projection. */3
import type { ToolHistory, ToolSchema } from '@deepseek-ai/dsh-llm'4
import { deepFreeze } from '@deepseek-ai/dsh-util-values'5
import type { SessionEvent, SessionSeq } from './types.ts'7
/** Folds committed headers and developer messages independently of model capability. */8
export class ToolHistoryProjection {9
/** Historical declarations indexed by the header sequence referenced by additions. */10
private readonly headers = new Map<SessionSeq, readonly ToolSchema[]>()11
/** Definitions retained in the current declaration series, including removed tools. */12
private declared = new Map<string, ToolSchema>()13
/** Active definitions from the latest request header. */14
private active: readonly ToolSchema[] = []15
/** Active names reconstructed from the baseline and recorded updates. */16
private available = new Set<string>()17
/** Header that starts the current declaration series; absent before the first header. */18
private baselineSeq: SessionSeq | undefined19
/** Immutable request snapshot of the current baseline and resolved updates. */20
private history: ToolHistory = deepFreeze({ tools: [], updates: [] })22
/**23
* Consume the next committed event in log order.24
* @param event - a session event, including inherited events during restoration.25
*/26
apply(event: SessionEvent): void {27
if (event.type === 'request/header') {28
const tools = event.data.header.tools ?? []29
this.headers.set(event.seq, tools)30
// A retained name cannot acquire a different definition without losing31
// prefix reuse; an unchanged restored name is re-offered by its recorded addition.32
const redeclared = tools.some((tool) => {33
const before = this.declared.get(tool.name)34
return before !== undefined && JSON.stringify(before) !== JSON.stringify(tool)35
})36
if (this.baselineSeq === undefined || event.data.reason === 'series' || event.data.startsSeries || redeclared) {37
this.baselineSeq = event.seq38
this.declared = new Map(tools.map(tool => [tool.name, tool]))39
this.history = deepFreeze({ tools, updates: [] })40
this.available = new Set(tools.map(tool => tool.name))41
}42
this.active = tools43
} else if (event.type === 'developer/message') {44
const { message, headerSeq } = event.data45
const definitions = headerSeq === undefined ? [] : this.headers.get(headerSeq)46
const additions = message.content.flatMap((block) => {47
if (block.type !== 'tool-addition') return []48
const tool = definitions?.find(tool => tool.name === block.toolName)49
if (tool === undefined) throw new Error(`tool history: missing definition for ${block.toolName}`)50
return [tool]51
})52
for (const tool of additions) this.declared.set(tool.name, tool)53
for (const block of message.content) {54
if (block.type === 'tool-addition') this.available.add(block.toolName)55
else if (block.type === 'tool-removal') this.available.delete(block.toolName)56
}57
this.history = deepFreeze({58
tools: this.history.tools,59
updates: [...this.history.updates, { messageId: message.id, additions }],60
})61
}62
}64
/**65
* Read an immutable snapshot; subsequent events do not mutate it.66
* @returns initial declarations and historically resolved additions for the current series.67
*/68
snapshot(): ToolHistory {69
// Sessions written before tool-update emission have headers without matching updates.70
if (this.active.length !== this.available.size || this.active.some(tool => !this.available.has(tool.name))) {71
return deepFreeze({ tools: this.active, updates: [] })72
}73
return this.history74
}75
}