72 lines
2.8 KiB
TypeScript
72 lines
2.8 KiB
TypeScript
/**
|
|
* Request-header reconstruction utilities over full `request/header` session
|
|
* events. Anyone holding a session log reconstructs the {@link EpochHeader}
|
|
* any request was built under by taking the latest canonical snapshot; the
|
|
* loop uses the same equality helper to avoid logging unchanged headers.
|
|
*
|
|
* @module dsh-session/request-header
|
|
*/
|
|
|
|
import { callConfigEquals } from '@deepseek-ai/dsh-llm'
|
|
import type { ToolSchema } from '@deepseek-ai/dsh-llm'
|
|
import type { EpochHeader, SessionEvent } from './types.ts'
|
|
|
|
/**
|
|
* Normalize a header to canonical form: an empty system prompt and empty tool
|
|
* list become absent fields, matching how requests are built. Logging, folding,
|
|
* and comparison use this one representation.
|
|
* @param header - the header to normalize (not mutated).
|
|
* @returns the canonical header.
|
|
*/
|
|
export function canonicalHeader(header: EpochHeader): EpochHeader {
|
|
const adapterDefaults = header.adapterDefaults
|
|
return {
|
|
config: header.config,
|
|
...adapterDefaults?.reasoningEffort === true || adapterDefaults?.maxTokens === true
|
|
? { adapterDefaults }
|
|
: {},
|
|
...header.system !== undefined && header.system.length > 0 ? { system: header.system } : {},
|
|
...header.tools !== undefined && header.tools.length > 0 ? { tools: header.tools } : {},
|
|
}
|
|
}
|
|
|
|
/** Canonical JSON equality for tool schemas assembled through the same path. */
|
|
function sameSchema(a: ToolSchema, b: ToolSchema): boolean {
|
|
return JSON.stringify(a) === JSON.stringify(b)
|
|
}
|
|
|
|
/**
|
|
* Field-wise equality over canonical headers. Tool schemas compare in order.
|
|
* @param a - one canonical header.
|
|
* @param b - the other.
|
|
* @returns whether config, system, and tools all match.
|
|
*/
|
|
export function headerEquals(a: EpochHeader, b: EpochHeader): boolean {
|
|
if (
|
|
!callConfigEquals(a.config, b.config)
|
|
|| a.adapterDefaults?.reasoningEffort !== b.adapterDefaults?.reasoningEffort
|
|
|| a.adapterDefaults?.maxTokens !== b.adapterDefaults?.maxTokens
|
|
|| a.system !== b.system
|
|
) return false
|
|
const at = a.tools ?? []
|
|
const bt = b.tools ?? []
|
|
return at.length === bt.length && at.every((tool, i) => sameSchema(tool, bt[i] as ToolSchema))
|
|
}
|
|
|
|
/**
|
|
* Fold the header events of a log (or any prefix) into the
|
|
* {@link EpochHeader} in force after the last snapshot. Non-header events are
|
|
* skipped. This is the pure offline reconstruction path; the live session
|
|
* tracks the same fold incrementally.
|
|
* @param events - session events in log order.
|
|
* @param from - a previously folded state to continue from.
|
|
* @returns the latest canonical header, or undefined when none exists yet.
|
|
*/
|
|
export function foldRequestHeader(events: readonly SessionEvent[], from?: EpochHeader): EpochHeader | undefined {
|
|
let state = from
|
|
for (const event of events) {
|
|
if (event.type === 'request/header') state = canonicalHeader(event.data.header)
|
|
}
|
|
return state
|
|
}
|