|
|
2 месяцев назад | |
|---|---|---|
| .. | ||
| src | 2 месяцев назад | |
| tests | 2 месяцев назад | |
| README.md | 2 месяцев назад | |
| package.json | 2 месяцев назад | |
| tsconfig.json | 2 месяцев назад | |
The basic compaction backend: a BasicCompactService implementing the @deepseek-ai/dsh-compact seam with a char/4 token heuristic, token-budget retention, and ctx.llm.stream() summarization.
This is the implementation tier of the compaction capability — see the interface package for the seam and the capability-seam RFC for the design.
The abstract contract states only WHAT compaction does; this backend owns every HOW decision:
estimateContentTokens(): char/4 with per-block structural overhead (text/reasoning = ceil(len/4) + 4, tool-call from name + arguments, tool-result recursive, image = 85, unknown blocks via JSON length).compactIfNeeded() walks the surface nodes tail→head summing per-node token estimates, and retains the smallest tail-run of WHOLE units (a closed step, or a single no-step node such as a pre-step user/message or inter-step steering/message) whose total reaches retainTokens; everything older is compacted. Retention is turn-agnostic — turn boundaries play no role, so a single runaway turn that alone exceeds the window compacts its OWN early closed steps rather than being retained verbatim (the failure mode that motivated dropping turn-protection: a tool-heavy turn must stay compactable or the harness dies exactly when compaction is needed). The only structural guard is step-alignment: the compacted region always ends on a step boundary, so it never splits a step's assistant/message tool-calls from their tool/results. When the only compactable content left is an un-splittable open tail step, it declines (returns null) and retries once an older step closes. Single-unit overflow is out of scope, by design: if one retained unit (a single closed step, or a large pasted user/message) ALONE exceeds the budget, compaction cannot help and the call may go out over-budget — bounding an individual unit's size is a separate concern. compactRegion() enforces step-alignment strictly, throwing on a boundary that would split a step.resolveConfig() rejects (throws) any config where summarizationMaxTokens + retainTokens > contextWindow * thresholdRatio. The invariant guarantees the post-compaction history (the bounded summary plus the retained recent tail) is structurally below the threshold, so a compaction never immediately triggers another: consecutive re-compaction is impossible by construction.summarize(): a ctx.llm.stream() call assembled via BlockAssembler (the single model-call surface) with a fixed system prompt that asks for a structured checkpoint (Primary Request and Intent · Key Technical Concepts · Files and Code · Errors and Fixes · Pending Tasks · Current Work · Next Step · Critical Context), every section mandatory, exact paths/commands/identifiers preserved. The compacted region is flattened to a plain-text transcript first: text and reasoning contribute their text, and every non-text block (image, tool-call, tool-result, plugin-added types) contributes a type-tagged placeholder ([image], [tool-call: name(args)], …) so the summarizer is told what existed rather than silently dropping it.compactRegion() wraps it in a checkpoint preamble (so a resuming model reads it as a checkpoint, not a fresh user request, and builds on the captured context rather than restating it) plus <compacted-summary>…</compacted-summary> tags. Because region compaction can be invoked manually, a surface may hold several checkpoints, so the framing does not claim everything after it is recent or verbatim. The tags make a prior checkpoint detectable in the transcript on the next compaction cycle: the summarization prompt then instructs the model to merge it in place (preserve still-true facts, drop stale ones) rather than re-summarize it verbatim — a cheap incremental merge that needs no extra log/event machinery. The unframed summary stays on the compact/summary provenance event.compactRegion() appends the compact/start → compact/summary → compact/end log records and the single user/message replace node carrying the framed summary (see the interface README).agent/pre-request listener delegates to compactIfNeeded() before every model call (every step, not just a turn's first — a tool-heavy turn grows the surface mid-turn, so a runaway turn still compacts, and per-step firing is the only moment to rescue it before overflow). agent/pre-request is an awaited surface-mutation checkpoint that fires BEFORE the loop derives the request history, so compaction mutates the surface and the loop derives once from the result — no double-derive, and the listener cannot see (or need to rewrite) an already-assembled messages array. The listener owns no threshold logic of its own (the single token-pressure check lives in compactIfNeeded()).compact/start … compact/end bracket is a log-recorded lock: it makes a crash mid-summarization a detectable orphan (a compact/start with no compact/end), records provenance, and prevents a concurrent compaction. Two failure paths: a crash (the loop dies mid-summarization) leaves a dangling compact/start that is inert — compact/* events are log-only, the surface replacement never landed, so the full history derives fine and generic turn-repair closes the turn; a recoverable failure (summarization throws but the loop survives) appends compact/end with its error field set, leaving the surface untouched so the call proceeds with full history. Core session repair stays compaction-agnostic by design — it never learns about compact/*.estimateContentTokens() and summarize() are overridable hooks: a tokenizer-based or template-based backend can subclass BasicCompactService and override just those, reusing the retention walk and surface plumbing.
BasicCompactConfig)| Key | Default | Meaning |
|---|---|---|
contextWindow |
128000 |
Context window size in tokens. |
thresholdRatio |
0.8 |
Compact when estimated usage exceeds this fraction of the window. |
retainTokens |
20480 |
Tokens of recent context to keep intact. |
summarizationModel |
'' |
Model for summarization (empty → use the agent's model). |
summarizationMaxTokens |
2048 |
Max tokens for the summary response. |
auto |
true |
Register the agent/pre-request auto-compaction listener. Set false for manual-only. |
import type { Context } from 'cordis'
import { BasicCompactService } from '@deepseek-ai/dsh-compact-basic'
export const name = 'compact-basic'
export const inject = ['llm']
export function apply(ctx: Context): void {
ctx.plugin(BasicCompactService, { contextWindow: 128000, retainTokens: 20480 })
}
Loading the plugin registers ctx.compact. With auto: true (the default) it compacts automatically under token pressure; a consumer (a future /compact tool) can also call ctx.compact.compactIfNeeded(...) or ctx.compact.compactRegion(...) directly.