breakdown-projection.ts 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. /**
  2. * Pure fold for the heuristic context-composition projection: system prompt
  3. * and tool schemas from the newest request envelope, conversation from the
  4. * live surface. Prices with the same shared estimator as the meter service,
  5. * so the three figures match `measure()`'s heuristic vocabulary exactly.
  6. */
  7. import { z } from 'zod'
  8. import { canonicalHeader } from '@deepseek-ai/dsh-session'
  9. import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
  10. import { estimateSystemTokens, estimateToolsTokens } from './estimate.ts'
  11. import { foldSurfaceProjection } from './surface-projection.ts'
  12. import type { ShadowPriceClaim } from './surface-projection.ts'
  13. // Import for the `contextBreakdown` SessionProjectionMap key merge.
  14. import type {} from './projection.ts'
  15. interface ContextBreakdownState {
  16. systemTokens: number
  17. toolsTokens: number
  18. messageTokens: number
  19. /** Shadow price armed by the immediately preceding metering event. */
  20. claim?: ShadowPriceClaim
  21. }
  22. const breakdownSchema = z.object({
  23. systemTokens: z.number().int().nonnegative(),
  24. toolsTokens: z.number().int().nonnegative(),
  25. messageTokens: z.number().int().nonnegative(),
  26. }).strict()
  27. /**
  28. * Token-meter's context-composition projection unit.
  29. *
  30. * Envelope figures are last-wins per `request/header`; the message figure
  31. * rides {@link foldSurfaceProjection} — the same O(1) fold the occupancy
  32. * projection uses — so fully metered logs equal `measure().surfaceTokens` at
  33. * every event boundary and compaction shrinks the figure by its logged shadow
  34. * price. A replacement without a claim preserves the previous total. The
  35. * state is a fixed handful of numbers, so the persisted checkpoint stays
  36. * O(1) over the session's life.
  37. */
  38. export const contextBreakdownProjectionDefinition:
  39. ProjectionDefinition<'contextBreakdown', ContextBreakdownState> = {
  40. key: 'contextBreakdown',
  41. schema: breakdownSchema,
  42. init: () => ({ systemTokens: 0, toolsTokens: 0, messageTokens: 0 }),
  43. apply: (state, event) => {
  44. const fold = foldSurfaceProjection(state.claim, event)
  45. let systemTokens = state.systemTokens
  46. let toolsTokens = state.toolsTokens
  47. if (event.type === 'request/header') {
  48. const header = canonicalHeader(event.data.header)
  49. systemTokens = estimateSystemTokens(header)
  50. toolsTokens = estimateToolsTokens(header)
  51. }
  52. if (systemTokens === state.systemTokens
  53. && toolsTokens === state.toolsTokens
  54. && fold.deltaTokens === 0
  55. && fold.claim === undefined
  56. && state.claim === undefined) return state
  57. return {
  58. systemTokens,
  59. toolsTokens,
  60. messageTokens: state.messageTokens + fold.deltaTokens,
  61. ...fold.claim === undefined ? {} : { claim: fold.claim },
  62. }
  63. },
  64. view: ({ systemTokens, toolsTokens, messageTokens }) => ({ systemTokens, toolsTokens, messageTokens }),
  65. stateVersion: 2,
  66. }