Files
deepseek-harness/packages/llm/llm/src/call-config.ts
Tianyi Cui 2093a8898b loop: every request is built from the log — boundary snapshot, header events, config-only waterfall
The loop is now transmission-stateless; a request is a pure function of
(session log, this step's rendered assembly, current AgentOptions):

- The reconstruction boundary is step/start: the messages snapshot is
  taken in the same synchronous frame immediately before the step/start
  append, so the request's messages are exactly the derivation over
  events[0..stepStartSeq) — an inject() from an agent/request listener
  (or any concurrent task) lands after the boundary and joins the NEXT
  request. This changes behavior for a synchronous step/start
  session/event listener that appends content (master derived after the
  append, so such a listener could reach the current request):
  agent/pre-step is the sanctioned seam for current-request content.
- agent/request is re-typed to config-only: (agent, turn, step,
  config: LlmCallConfig, next) → LlmCallConfig. The frozen seed comes
  from AgentOptions on a loop instance's first request (explicit options
  beat the logged baseline — fork overrides and resume reconfiguration
  stay correct) and from the log's folded header afterwards; listeners
  return a replacement to switch. Content shaping through the request is
  no longer expressible — model-visible content flows through the log
  channels.
- recordRequestHeader appends whatever header event the request owes the
  log before dispatch: an 'initial'/'resume' snapshot anchoring each
  loop instance, a round-trip-verified delta on change, a 'fallback'
  snapshot when the encoding cannot express it. Session.requestHeader()
  is the log's incrementally-folded baseline.
- Requests are deep-frozen before dispatch (deepFreeze exempts the
  AbortSignal — freezing one breaks AbortController.abort() outright);
  frozen + sessionId is the loop-built marker the dev invariant keys on.

Ported from #162 and re-anchored on the log: the append-extension /
frozen-end-to-end / compaction-resend / prompt-change property tests,
plus new specs for the boundary semantics, resume anchoring, and the
end-to-end theorem (every recorded request rebuilds byte-equal from the
log alone). Live cache-hit e2e (request-cache.e2e.ts) verified against
the real DeepSeek API. Snapshot goldens intentionally stale until the
single re-record after the compact/summary envelope lands.
2026-07-06 03:07:34 +08:00

70 lines
3.0 KiB
TypeScript

/**
* The call configuration of a conversation and its comparison/freeze
* utilities. `LlmCallConfig` is the non-content third of the request header
* (see `EpochHeader` in dsh-session): everything about a request besides its
* message content that can undermine provider KV-cache reuse — `model`
* selects the cache namespace outright, and the sampling scalars are treated
* the same way out of caution. It is per-conversation state recorded in the
* session log (the reconstructability RFC), never a silently-drifting
* per-call knob: the `agent/request` waterfall proposes a replacement, and
* the loop logs a real change as a `request/header-delta` event.
*
* @module dsh-llm/call-config
*/
/**
* Model + sampling scalars of one conversation's requests. Every field maps
* 1:1 onto the same-named `GenerateOptions` field; the loop builds requests
* from the logged header rather than accepting these per call.
*/
export interface LlmCallConfig {
model: string
temperature?: number
maxTokens?: number
stop?: string[]
}
/**
* Field-wise equality over {@link LlmCallConfig} — the comparison a caller
* runs to decide whether a proposed configuration is a real change (worth a
* logged header delta) or the held one restated.
* @param a - one configuration.
* @param b - the other.
* @returns whether every field (including the `stop` list, element-wise) matches.
*/
export function callConfigEquals(a: LlmCallConfig, b: LlmCallConfig): boolean {
if (a.model !== b.model || a.temperature !== b.temperature || a.maxTokens !== b.maxTokens) return false
if (a.stop === undefined || b.stop === undefined) return a.stop === b.stop
return a.stop.length === b.stop.length && a.stop.every((s, i) => s === b.stop?.[i])
}
/**
* Deep-freeze a value in place so any later mutation throws (ESM code runs in
* strict mode), and return it. The loop freezes every request it builds
* before dispatch — `llm/stream` listeners and adapters read the request,
* never rewrite it, so the wire bytes cannot silently desync from what the
* session log reconstructs. Guards against cycles with a WeakSet: loop-built
* requests hold `structuredClone`d JSON-validated session data, but the
* helper accepts arbitrarily constructed values. One exemption: an
* `AbortSignal` is never entered or frozen — it is the request's live
* cancellation channel, and freezing one breaks `AbortController.abort()`
* outright (Node stores the aborted flag as an own property of the signal).
* @param value - the value to freeze in place.
* @returns the same value, frozen.
*/
export function deepFreeze<T>(value: T): T {
const seen = new WeakSet<object>()
const walk = (node: unknown): void => {
if (node === null || typeof node !== 'object') return
if (node instanceof AbortSignal) return
if (seen.has(node)) return
seen.add(node)
Object.freeze(node)
for (const key of Object.keys(node)) {
walk((node as Record<string, unknown>)[key])
}
}
walk(value)
return value
}