surface-fold.ts 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. /**
  2. * The measurement service's positional surface fold: the per-node priced
  3. * surface `measure()` serves and compaction plans against. The projection
  4. * units deliberately do NOT share this fold — their state must stay O(1)
  5. * for the persisted checkpoint, so they ride `surface-projection.ts`'s
  6. * shadow-price protocol instead. The two stay in agreement by construction:
  7. * both price through `estimate.ts`, and every logged shadow price is derived
  8. * from THIS fold's nodes by the replace producer.
  9. *
  10. * @module @deepseek-ai/dsh-token-meter/surface-fold
  11. */
  12. import { deriveEventMessage } from '@deepseek-ai/dsh-session'
  13. import type { SurfaceEvent } from '@deepseek-ai/dsh-session'
  14. import type { TokenSurfaceNode } from './types.ts'
  15. import { estimateMessage } from './estimate.ts'
  16. /** One surface event's placement and cost against the surface preceding it. */
  17. export interface SurfaceTokenFold {
  18. /** Heuristic price of the event's own message; 0 when it derives none. */
  19. readonly tokens: number
  20. /** The surface after the event, detached from the input. */
  21. readonly nodes: TokenSurfaceNode[]
  22. /** Signed change in the surface total: `tokens` minus anything shadowed. */
  23. readonly deltaTokens: number
  24. }
  25. /**
  26. * Fold one surface event onto a priced surface.
  27. *
  28. * Total and allocation-fresh: the caller assigns the result rather than
  29. * mutating in place, so a throw here leaves the caller's state untouched and
  30. * the same malformed event fails identically on every retry.
  31. * @param nodes - the priced surface preceding this event, in model-visible order.
  32. * @param event - the surface event to place.
  33. * @returns the event's price, the next surface, and the signed total delta.
  34. * @throws when a replacement names a range absent from `nodes` — committed
  35. * logs are surface-validated at append time, so an unresolvable range is log
  36. * corruption and must fail loud rather than skip the event.
  37. */
  38. export function foldSurfaceTokens(
  39. nodes: readonly TokenSurfaceNode[],
  40. event: SurfaceEvent,
  41. ): SurfaceTokenFold {
  42. const message = deriveEventMessage(event)
  43. const tokens = message === null ? 0 : estimateMessage(message)
  44. const op = event.surfaceOp
  45. if (op === 'append') {
  46. return { tokens, nodes: [...nodes, { seq: event.seq, tokens }], deltaTokens: tokens }
  47. }
  48. const startIdx = nodes.findIndex(node => node.seq === op.start)
  49. const endIdx = nodes.findIndex(node => node.seq === op.end)
  50. if (startIdx === -1 || endIdx === -1 || startIdx > endIdx) {
  51. throw new Error(
  52. `token surface: replace at seq ${event.seq} has invalid current range ${op.start}-${op.end}`,
  53. )
  54. }
  55. const removed = nodes
  56. .slice(startIdx, endIdx + 1)
  57. .reduce((total, node) => total + node.tokens, 0)
  58. const next = [...nodes]
  59. next.splice(startIdx, endIdx - startIdx + 1, { seq: event.seq, tokens })
  60. return { tokens, nodes: next, deltaTokens: tokens - removed }
  61. }