index.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  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). A tokenizer-, template-, or model-backed implementation can live
  10. * as a sibling package; callers stay on the same `ctx.compact` seam without
  11. * touching consumers.
  12. *
  13. * The split follows the capability-seams RFC — interface (this) /
  14. * implementation (deferred) / consumer (a `/compact` tool, deferred) — modeled
  15. * 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/implemented/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. /** Minimal agent context compaction needs without depending on the agent package. */
  28. export interface CompactAgentContext {
  29. session: Session
  30. options: { model?: string }
  31. }
  32. declare module 'cordis' {
  33. interface Context {
  34. compact: CompactService
  35. }
  36. }
  37. /**
  38. * Abstract compaction service. Subclass implement the two abstract methods,
  39. * and load the subclass as a plugin — it registers as `ctx.compact` (one
  40. * implementation per context; loading a second throws, which is cordis'
  41. * standard duplicate-service behavior).
  42. *
  43. * Both core methods are abstract: the contract states WHAT compaction does,
  44. * while the entire strategy — token estimation, retention policy, event
  45. * sequencing, summarization — is a HOW decision owned by the implementation.
  46. *
  47. * Implementations MUST honor:
  48. * - **Surface contract**: a successful compaction shadows the compacted surface
  49. * nodes with a SINGLE replacement node carrying the summary. Because
  50. * `SurfaceEventType` is a closed union, that node is a `user/message` with
  51. * `surfaceOp: { op:'replace', start, end }`; the `compact/*` events are
  52. * log-only (lock + provenance).
  53. * - **Blocking**: no compaction begins while another is in progress for the
  54. * same session. The recommended mechanism is the log-recorded lock — append
  55. * `compact/start` before the slow work and `compact/end` after (even on
  56. * failure) — so the lock is visible to replay and crash recovery.
  57. */
  58. export abstract class CompactService extends Service {
  59. constructor(ctx: Context) {
  60. super(ctx, 'compact')
  61. }
  62. /**
  63. * Check token pressure and compact if the conversation is too large.
  64. *
  65. * Estimates the current surface-derived history size (including the system
  66. * prompt), and if it exceeds the backend's threshold, compacts an older range
  67. * via {@link compactRegion}, keeping recent context intact. Returns `null`
  68. * when no compaction is needed.
  69. *
  70. * Scope and guarantees a backend MUST honor:
  71. * - **Surface-derived history only.** The decision is made against the history
  72. * derived from the session surface — the only thing compaction can act on.
  73. * Non-surface context injected downstream (into the request `messages` by a
  74. * later listener) is out of this accounting by construction.
  75. * - **Head-anchored, best-effort.** Auto-compaction consolidates from the
  76. * surface HEAD up to a balanced tool-pairing cutoff, so a prior head
  77. * checkpoint is
  78. * re-summarized into one fresh checkpoint (the surface holds at most one
  79. * auto-generated checkpoint, always at the head). It is best-effort over
  80. * CLOSED steps: when the only compactable content left is an un-splittable
  81. * open tail step, it declines (`null`) and retries once that step closes.
  82. * - **Single-unit overflow is out of scope.** If a single retained unit (one
  83. * closed step, or a large free node such as a pasted `user/message`) ALONE
  84. * exceeds the budget, compaction cannot help and the call may go out
  85. * over-budget. Bounding an individual unit's size is a separate concern.
  86. *
  87. * @param agent - agent context owning the session surface and model options.
  88. * @param fullSystemPrompt - assembled system prompt, counted toward the estimate.
  89. * @param signal - cancellation signal. A backend summarizing via
  90. * `ctx.llm.stream()` MUST forward this into the call's `GenerateOptions.signal`
  91. * so an abort/dispose tears down the in-flight summarization rather than
  92. * leaving an orphaned model call running past the cancellation.
  93. * @returns the compaction result, or `null` if no compaction was needed.
  94. */
  95. abstract compactIfNeeded(
  96. agent: CompactAgentContext,
  97. fullSystemPrompt: string,
  98. signal: AbortSignal,
  99. ): Promise<CompactionResult | null>
  100. /**
  101. * Forcibly compact a range of surface nodes into a single summary node.
  102. *
  103. * `start` and `end` are inclusive seqs of surface nodes to shadow; the backend
  104. * summarizes their content and appends a replacement surface node. Used by the
  105. * (future) `/compact` tool and internally by {@link compactIfNeeded}.
  106. *
  107. * The region MUST NOT split a step's `assistant/message` tool-calls from their
  108. * `tool/result`s, leaving the rehydrated transcript with a dangling tool-call
  109. * or an orphaned tool-result that every provider rejects. A region is safe iff
  110. * both its edges are balanced cuts on the surface: the cut before `start` and
  111. * the cut after `end` each have no unanswered tool-call before them. A node
  112. * that belongs to no step (a pre-step user message, inter-step steering, or an
  113. * injection context message) is a balanced (free) boundary; an `end` inside an
  114. * open (unclosed) tail step is invalid — its tool-calls have no results yet.
  115. * `dsh-session` exports `isToolPairingBalanced` for this check.
  116. *
  117. * @param session - the session whose surface is mutated.
  118. * @param start - inclusive seq of the first surface node to compact.
  119. * @param end - inclusive seq of the last surface node to compact.
  120. * @param agent - agent context used by router-aware summarizers.
  121. * @param signal - optional cancellation signal. A backend that summarizes via
  122. * `ctx.llm.stream()` MUST forward this into the call's `GenerateOptions.signal`
  123. * so an abort/dispose tears down the in-flight summarization rather than
  124. * leaving an orphaned model call running past the cancellation.
  125. * @throws if compaction is already in progress, if `start`/`end` are not
  126. * valid surface nodes, if `start` is positioned after `end` on the surface
  127. * (the range is a surface-POSITION span, not a numeric seq interval — a
  128. * prior replace can leave the surface non-monotonic in seq order), or if
  129. * either boundary is not a balanced tool-pairing cut (would split a step's
  130. * tool-call/result pair).
  131. * @returns what the compaction did (the replaced range and its summary node).
  132. */
  133. abstract compactRegion(
  134. session: Session,
  135. start: number,
  136. end: number,
  137. agent: CompactAgentContext,
  138. signal?: AbortSignal,
  139. ): Promise<CompactionResult>
  140. }
  141. export default CompactService