|
|
|
|
@@ -1,6 +1,6 @@
|
|
|
|
|
# Compaction
|
|
|
|
|
|
|
|
|
|
The compaction seam — a [capability seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md) split like bash: interface ([dsh-compact](../../packages/compact/compact), `ctx.compact`), implementation (a backend such as `dsh-compact-basic`, deferred), and consumer (a `/compact` tool, deferred). Compaction is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). A tokenizer- or template-based backend is a sibling package implementing the same interface. Unlike bash, the interface necessarily depends on `dsh-session` and `dsh-llm`: its verbs are defined over a `Session` and its output is the `ContentBlock` vocabulary (see the [compaction capability-seam RFC](../rfc/proposed/feature/2026-06-18-compaction-capability-seam.md)).
|
|
|
|
|
The compaction seam — a [capability seam](../rfc/implemented/architecture/2026-06-13-capability-seams.md) split like bash: interface ([dsh-compact](../../packages/compact/compact), `ctx.compact`), implementation (a backend such as [dsh-compact-basic](../../packages/compact/compact-basic)), and consumer (a `/compact` tool, deferred). Compaction is **one optional capability**, not part of the agent-loop spine — so its vocabulary lives here, not in [core.md](core.md). A tokenizer- or template-based backend is a sibling package implementing the same interface. Unlike bash, the interface necessarily depends on `dsh-session` and `dsh-llm`: its verbs are defined over a `Session` and its output is the `ContentBlock` vocabulary (see the [compaction capability-seam RFC](../rfc/implemented/feature/2026-06-18-compaction-capability-seam.md)).
|
|
|
|
|
|
|
|
|
|
Source: [`packages/compact/compact/src/types.ts`](../../packages/compact/compact/src/types.ts)
|
|
|
|
|
|
|
|
|
|
@@ -11,7 +11,7 @@ Compaction extends [`SessionEventMap`](session.md) with three event types via de
|
|
|
|
|
| Event | Payload | Role |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| `compact/start` | `{ turn }` | acquires the log-recorded lock |
|
|
|
|
|
| `compact/summary` | `{ summary, shadowedRange, shadowedSeqs, shadowedTokenCount }` | provenance: the summary blocks, the shadowed seq range, and the estimated token count |
|
|
|
|
|
| `compact/summary` | `{ summary, shadowedRange, shadowedSeqs, shadowedTokenCount }` | provenance: the summary blocks, the shadowed surface-boundary pair (`start`/`end` seqs — a position span, not a numeric interval), the shadowed seqs in surface order, and the estimated token count |
|
|
|
|
|
| `compact/end` | `{ turn, error? }` | releases the lock (`error` set when summarization threw) |
|
|
|
|
|
|
|
|
|
|
The lock brackets the **whole** operation: `compact/start` is appended first, then summarization, the `compact/summary` provenance record, and the `user/message` replacement all land, and only then `compact/end`. Releasing the lock last turns a crash mid-operation into a detectable orphaned lock (a `compact/start` with no matching `compact/end`) rather than a `compact/end` that falsely claims compaction finished.
|
|
|
|
|
@@ -32,9 +32,16 @@ interface CompactionResult {
|
|
|
|
|
endSeq: number
|
|
|
|
|
/** The summary content blocks produced by the backend. */
|
|
|
|
|
summary: ContentBlock[]
|
|
|
|
|
/** The seq range that was shadowed [start, end] inclusive. */
|
|
|
|
|
/**
|
|
|
|
|
* The surface-boundary pair that was shadowed: the seqs of the first
|
|
|
|
|
* (`start`) and last (`end`) surface nodes of the replaced range. A
|
|
|
|
|
* surface-POSITION span, not a numeric seq interval — after a prior replace
|
|
|
|
|
* lands a fresh high-seq summary node at an older range's position, `start`
|
|
|
|
|
* can be GREATER than `end`. {@link CompactionResult.shadowedSeqs} is the
|
|
|
|
|
* authoritative set of shadowed nodes, in surface order.
|
|
|
|
|
*/
|
|
|
|
|
shadowedRange: { start: number; end: number }
|
|
|
|
|
/** The seq numbers of all shadowed surface nodes. */
|
|
|
|
|
/** The seqs of all shadowed surface nodes, in surface order. */
|
|
|
|
|
shadowedSeqs: number[]
|
|
|
|
|
/** Estimated token count of the shadowed content. */
|
|
|
|
|
shadowedTokenCount: number
|
|
|
|
|
@@ -43,4 +50,6 @@ interface CompactionResult {
|
|
|
|
|
|
|
|
|
|
## The service
|
|
|
|
|
|
|
|
|
|
`CompactService` (`ctx.compact`, abstract — defined in [`packages/compact/compact/src/index.ts`](../../packages/compact/compact/src/index.ts)) declares two abstract methods: `compactIfNeeded(session, systemPrompt?, model?, signal?)` checks token pressure and compacts an older range if the history is too large (returning `null` when nothing needs it), and `compactRegion(session, start, end, model, signal?)` forcibly summarizes surface nodes `[start, end]` into a single replacement node. Both take an optional `signal: AbortSignal` that a backend summarizing via `ctx.llm.stream()` must forward into the call's `GenerateOptions.signal`, so an abort or dispose tears down the in-flight summarization. The entire strategy — token estimation, retention policy, event sequencing, summarization — is a HOW decision owned by the implementation.
|
|
|
|
|
`CompactService` (`ctx.compact`, abstract — defined in [`packages/compact/compact/src/index.ts`](../../packages/compact/compact/src/index.ts)) declares two abstract methods: `compactIfNeeded(agent, turn, step, fullSystemPrompt, signal)` checks token pressure and compacts an older range if the history is too large (returning `null` when nothing needs it), and `compactRegion(session, start, end, agent, turn, step, signal?)` forcibly summarizes surface nodes `[start, end]` into a single replacement node. `compactIfNeeded`'s parameters are all required — the loop's `agent/pre-step` checkpoint supplies the agent, lifecycle context, assembled `fullSystemPrompt`, and turn `signal`. A backend summarizing via `ctx.llm.stream()` must forward `signal` into the call's `GenerateOptions.signal`, so an abort or dispose tears down the in-flight summarization. The entire strategy — token estimation, retention policy, event sequencing, summarization — is a HOW decision owned by the implementation.
|
|
|
|
|
|
|
|
|
|
Auto-compaction runs on the serial `agent/pre-step` loop seam (fired once per step, after `turn/start` and BEFORE the step opens and its request history is derived), not the `agent/request` waterfall: compaction mutates the session surface in place — with its log-only `compact/*` records landing cleanly outside any step — and the loop derives the request from the already-compacted surface. Retention is turn-agnostic — the only structural guard is tool-pairing balance (a compacted region's edges are balanced cuts on the surface, so it never splits a step's tool-calls from their results), so a single runaway turn that alone exceeds the window compacts its own early closed steps rather than being retained verbatim. The backend that ships this (`dsh-compact-basic`) documents the retention walk, summary shrink validation, bounded re-compaction, and the crash/recoverable failure taxonomy.
|
|
|
|
|
|