index.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. /**
  2. * The compaction service seam (`ctx.compact`): an abstract service defining
  3. * WHAT compaction does — decide when to compact, summarize a range of
  4. * conversation history into a single surface node — without saying HOW.
  5. *
  6. * Implementations subclass {@link CompactService}, implement
  7. * {@link CompactService.compactIfNeeded} and {@link CompactService.compactRegion},
  8. * and load as a plugin — registering as `ctx.compact` (one implementation per
  9. * context). `@deepseek-ai/dsh-compact-basic` (char/4 estimation + token-budget
  10. * retention + `ctx.llm.stream()` summarization) is the first. A tokenizer- or
  11. * template-based backend swaps in without touching consumers.
  12. *
  13. * The split follows the capability-seams RFC — interface (this) /
  14. * implementation (`dsh-compact-basic`) / consumer (a `/compact` tool, deferred)
  15. * — modeled on the bash trio. Unlike `dsh-bash`, this interface necessarily
  16. * depends on `dsh-session` and `dsh-llm`: the contract's verbs are defined over
  17. * a `Session` and its output is the `ContentBlock` vocabulary. That deviation
  18. * from the "interface depends only on cordis" guidance is intentional and
  19. * recorded in the [compaction capability-seam RFC](../../../../docs/rfc/proposed/feature/2026-06-18-compaction-capability-seam.md).
  20. *
  21. * @module @deepseek-ai/dsh-compact
  22. */
  23. import { Context, Service } from 'cordis'
  24. import type { Session } from '@deepseek-ai/dsh-session'
  25. import type { CompactionResult } from './types.ts'
  26. export type { CompactionResult } from './types.ts'
  27. declare module 'cordis' {
  28. interface Context {
  29. compact: CompactService
  30. }
  31. }
  32. /**
  33. * Abstract compaction service. Subclass implement the two abstract methods,
  34. * and load the subclass as a plugin — it registers as `ctx.compact` (one
  35. * implementation per context; loading a second throws, which is cordis'
  36. * standard duplicate-service behavior).
  37. *
  38. * Both core methods are abstract: the contract states WHAT compaction does,
  39. * while the entire strategy — token estimation, retention policy, event
  40. * sequencing, summarization — is a HOW decision owned by the implementation.
  41. *
  42. * Implementations MUST honor:
  43. * - **Surface contract**: a successful compaction shadows the compacted surface
  44. * nodes with a SINGLE replacement node carrying the summary. Because
  45. * `SurfaceEventType` is a closed union, that node is a `user/message` with
  46. * `surfaceOp: { op:'replace', start, end }`; the `compact/*` events are
  47. * log-only (lock + provenance).
  48. * - **Blocking**: no compaction begins while another is in progress for the
  49. * same session. The recommended mechanism is the log-recorded lock — append
  50. * `compact/start` before the slow work and `compact/end` after (even on
  51. * failure) — so the lock is visible to replay and crash recovery.
  52. */
  53. export abstract class CompactService extends Service {
  54. constructor(ctx: Context) {
  55. super(ctx, 'compact')
  56. }
  57. /**
  58. * Check token pressure and compact if the conversation is too large.
  59. *
  60. * Estimates the current history size (optionally including a system prompt),
  61. * and if it exceeds the backend's threshold, compacts an older range via
  62. * {@link compactRegion}, keeping recent context intact.
  63. *
  64. * @param session - the session whose surface may be compacted.
  65. * @param systemPrompt - optional system prompt, counted toward the estimate.
  66. * @param model - optional summarization model (falls back to backend config).
  67. * @param signal - optional cancellation signal. A backend that summarizes via
  68. * `ctx.llm.stream()` MUST forward this into the call's `GenerateOptions.signal`
  69. * so an abort/dispose tears down the in-flight summarization rather than
  70. * leaving an orphaned model call running past the cancellation.
  71. * @returns the compaction result, or `null` if no compaction was needed.
  72. */
  73. abstract compactIfNeeded(
  74. session: Session,
  75. systemPrompt?: string,
  76. model?: string,
  77. signal?: AbortSignal,
  78. ): Promise<CompactionResult | null>
  79. /**
  80. * Forcibly compact a range of surface nodes into a single summary node.
  81. *
  82. * `start` and `end` are inclusive seqs of surface nodes to shadow; the backend
  83. * summarizes their content and appends a replacement surface node. Used by the
  84. * (future) `/compact` tool and internally by {@link compactIfNeeded}.
  85. *
  86. * @param session - the session whose surface is mutated.
  87. * @param start - inclusive seq of the first surface node to compact.
  88. * @param end - inclusive seq of the last surface node to compact.
  89. * @param model - summarization model.
  90. * @param signal - optional cancellation signal. A backend that summarizes via
  91. * `ctx.llm.stream()` MUST forward this into the call's `GenerateOptions.signal`
  92. * so an abort/dispose tears down the in-flight summarization rather than
  93. * leaving an orphaned model call running past the cancellation.
  94. * @throws if compaction is already in progress, or if `start`/`end` are not
  95. * valid surface nodes, or if `start > end`.
  96. */
  97. abstract compactRegion(
  98. session: Session,
  99. start: number,
  100. end: number,
  101. model: string,
  102. signal?: AbortSignal,
  103. ): Promise<CompactionResult>
  104. }
  105. export default CompactService