refactor(compact): align pruning contracts (round 3)

This commit is contained in:
Hypatia May
2026-07-16 18:51:26 +08:00
parent 171bae5c20
commit ba4a57c7d1
38 changed files with 97 additions and 111 deletions

View File

@@ -6,7 +6,7 @@ A compaction capability family (see [capability seams](../../docs/rfc/implemente
|---|---|---|
| `compact/` | Abstract compaction seam (interface + `compact/*` events + `CompactionResult`) | `ctx.compact` |
| `compact-basic/` | A backend: `ctx.tokenMeter` pressure + token-budget retention + `llm.stream()` summarization | (registers `ctx.compact`) |
| `tool-result-prune/` | Optional model-free head/middle/tail rewriting before summary compaction | `ctx.toolResultPrune` |
| `compact-tool-result-prune/` | Optional model-free head/middle/tail rewriting before summary compaction | `ctx.toolResultPrune` |
| `tool-compact/` (deferred) | Model-facing `/compact` tool over `ctx.compact` | (registers on `ctx.tools`) |
The interface lives at `compact/compact/`, the backend at `compact/compact-basic/`, and deterministic pruning at `compact/tool-result-prune/`. Unlike the bash seam, the interface depends on `dsh-session` and `dsh-llm` because its verbs are defined over a `Session` and its output uses `ContentBlock`. That deviation is recorded in the [compaction capability-seam RFC](../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md). Token measurement remains a reusable LLM-family service; a template- or model-backed compactor can replace `compact-basic` without changing the meter, pruner, or callers.
The interface lives at `compact/compact/`, the backend at `compact/compact-basic/`, and deterministic pruning at `compact/compact-tool-result-prune/`. Unlike the bash seam, the interface depends on `dsh-session` and `dsh-llm` because its verbs are defined over a `Session` and its output uses `ContentBlock`. That deviation is recorded in the [compaction capability-seam RFC](../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md). Token measurement remains a reusable LLM-family service; a template- or model-backed compactor can replace `compact-basic` without changing the meter, pruner, or callers.

View File

@@ -9,8 +9,8 @@ This is the implementation tier of the compaction capability — see the [interf
This backend owns the compaction policy:
- **Measurement** — the singleton `ctx.tokenMeter` prices the latest canonical logged envelope and current surface at one consumed-log revision. Post-step pressure therefore includes the actual system prompt, tools, prefix, routing, assistant completion, tool results, buffered context, and steering.
- **Model-free pruning** — after pressure or canonical overflow qualifies, the optional [`ctx.toolResultPrune`](../tool-result-prune/README.md) service rewrites oversized tool results before range selection. Compact-basic remeasures through `ctx.tokenMeter`, skips summarization when pressure becomes safe, and otherwise summarizes the pruned surface. Below-pressure post-step checks never prune.
- **Retention** — compact the oldest whole surface units while preserving a recent tail and balanced tool-call/result cuts through the [`dsh-compact` boundary helpers](../compact/README.md#tool-pairing-boundaries). Turn boundaries do not protect old steps inside a runaway turn. An open indivisible tail declines until it closes; a single unit larger than the budget remains out of scope.
- **Model-free pruning** — after pressure or canonical overflow qualifies, the optional [`ctx.toolResultPrune`](../compact-tool-result-prune/README.md) service rewrites oversized tool results before range selection. Compact-basic remeasures through `ctx.tokenMeter`, skips summarization when pressure becomes safe, and otherwise summarizes the pruned surface. Below-pressure post-step checks never prune.
- **Retention** — compact the oldest whole surface units while preserving a recent tail and balanced tool-call/result cuts through the [`dsh-compact` boundary helpers](../compact/README.md#tool-pairing-boundaries). Turn boundaries do not protect old steps inside a runaway turn. An open indivisible tail declines until it closes. The optional pruner can repair an oversized closed tool unit when its text-bearing result is the removable bulk; indivisible non-tool units and non-prunable tool remainders remain out of scope.
- **Convergence** — retry head-checkpoint compaction up to `compactionRetries`; reject a summary that does not shrink its source, and throw if retries cannot return below threshold.
- **Summarization** — a direct `llm/stream` call uses the configured model and cap without running the loop-only `agent/request` seam. The input transcript preserves non-text blocks as tagged placeholders; only returned text enters the checkpoint, excluding reasoning and tool calls that would leak private reasoning or create an orphaned call.
- **Framing** — the replacement user message marks established checkpoint context with `<compacted-summary>` tags. The raw summary remains on the provenance event, and later automatic cycles merge the prior checkpoint.
@@ -50,7 +50,7 @@ export function apply(ctx: Context): void {
}
```
Loading the plugin registers `ctx.compact`. Add [`dsh-tool-result-prune`](../tool-result-prune/README.md) as a sibling before this plugin to enable the optional model-free pass. 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.
Loading the plugin registers `ctx.compact`. Add [`dsh-compact-tool-result-prune`](../compact-tool-result-prune/README.md) as a sibling before this plugin to enable the optional model-free pass. 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.
## Model Experience
@@ -120,7 +120,7 @@ Rules:
- **Meter accuracy follows the fixed heuristic** — missing reusable provider usage falls back to character count plus structural overhead rather than exact tokenization.
- **Overflow classification is adapter-maintained** — provider wording can change; both DeepSeek adapters normalize currently recognized context-limit failures to `CONTEXT_WINDOW_EXCEEDED`.
- **Single-unit and envelope-only overflow remain outside surface compaction** — recovery cannot split one indivisible message/tool unit or shrink system/tools/prefix.
- **Some indivisible-unit and envelope-only overflow remains outside surface compaction** — recovery cannot shrink system/tools/prefix, split an indivisible non-tool node, or repair a tool unit whose non-prunable remainder still exceeds the window. The optional pruner can shrink text-bearing tool-result bulk inside an otherwise indivisible pair.
- **`compactRegion` requires an open turn** — a manual call on a fully-closed session throws ("no open turn") rather than compacting.
- **Summarization failure fails closed with full, over-budget history** — including truncation at the summarization `maxTokens`, which hidden reasoning tokens can consume; the auto path logs a warning and proceeds.
- **Summarization failure preserves the latest durable surface** — before any replacement, the auto path logs a warning and proceeds with full over-budget history. If pruning already landed, a later summarization failure proceeds from that durable pruned surface. Summarization truncation at `maxTokens`, which hidden reasoning tokens can consume, follows the same rule.
- **The summarization call has no transcript-snapshot coverage** — `dsh-llm-replay` derives calls from `assistant/chunk` events, so this chunk-less direct `ctx.llm.stream()` call cannot replay (named deferred replay infrastructure in [the seam RFC](../../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md)).

View File

@@ -27,11 +27,11 @@
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-token-meter": "^0.0.1",
"@deepseek-ai/dsh-tool-result-prune": "^0.0.1",
"@deepseek-ai/dsh-compact-tool-result-prune": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"peerDependenciesMeta": {
"@deepseek-ai/dsh-tool-result-prune": {
"@deepseek-ai/dsh-compact-tool-result-prune": {
"optional": true
}
},
@@ -49,7 +49,7 @@
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-token-meter": "workspace:^",
"@deepseek-ai/dsh-tool-result-prune": "workspace:^",
"@deepseek-ai/dsh-compact-tool-result-prune": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"cordis": "^4.0.0-rc.7"
}

View File

@@ -13,7 +13,7 @@ import { CONTEXT_WINDOW_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { Agent } from '@deepseek-ai/dsh-agent'
// Type-only: makes the optional sibling service available to `ctx.get()`.
import type {} from '@deepseek-ai/dsh-tool-result-prune'
import type {} from '@deepseek-ai/dsh-compact-tool-result-prune'
import { resolveConfig } from './config.ts'
import { compactSurfaceRegion, selectCompactableRange } from './region.ts'
import { summarizeWithLlm } from './summarizer.ts'

View File

@@ -9,7 +9,7 @@ import LlmService, { CallId, CONTEXT_WINDOW_EXCEEDED_CODE, LlmAdapter } from '@d
import type { ContentBlock, GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
import ToolResultPruneService from '@deepseek-ai/dsh-tool-result-prune'
import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune'
import type { Agent } from '@deepseek-ai/dsh-agent'
const SIGNAL = new AbortController().signal

View File

@@ -9,7 +9,7 @@ import Include from '@cordisjs/plugin-include'
import LlmService from '@deepseek-ai/dsh-llm'
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
import BasicCompactService from '@deepseek-ai/dsh-compact-basic'
import ToolResultPruneService from '@deepseek-ai/dsh-tool-result-prune'
import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune'
let root: string | undefined
let context: Context | undefined
@@ -33,7 +33,7 @@ async function loadYaml(lines: readonly string[]): Promise<Context> {
const modules = new Map<string, unknown>([
['@deepseek-ai/dsh-llm', LlmService],
['@deepseek-ai/dsh-token-meter', TokenMeterService],
['@deepseek-ai/dsh-tool-result-prune', ToolResultPruneService],
['@deepseek-ai/dsh-compact-tool-result-prune', ToolResultPruneService],
['@deepseek-ai/dsh-compact-basic', BasicCompactService],
])
context.loader.internal = {
@@ -58,7 +58,7 @@ describe('real Loader composition', () => {
"- name: '@deepseek-ai/dsh-token-meter'",
' config:',
' contextWindow: 4096',
"- name: '@deepseek-ai/dsh-tool-result-prune'",
"- name: '@deepseek-ai/dsh-compact-tool-result-prune'",
' config:',
' thresholdChars: 100',
' headChars: 20',

View File

@@ -14,6 +14,6 @@
{ "path": "../../core/session" },
{ "path": "../../core/agent" },
{ "path": "../compact" },
{ "path": "../tool-result-prune" }
{ "path": "../compact-tool-result-prune" }
]
}

View File

@@ -1,4 +1,4 @@
# @deepseek-ai/dsh-tool-result-prune
# @deepseek-ai/dsh-compact-tool-result-prune
The replay-safe model-free pruning service (`ctx.toolResultPrune`). It rewrites over-budget `tool/result` surface nodes to a bounded head, a fixed omission marker, and a bounded tail while retaining the full original event in the append-only session log.
@@ -28,7 +28,7 @@ All values are integers; the threshold is positive and head/tail are non-negativ
```ts
import type { Context } from 'cordis'
import ToolResultPruneService from '@deepseek-ai/dsh-tool-result-prune'
import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune'
export function apply(ctx: Context): void {
ctx.plugin(ToolResultPruneService)

View File

@@ -1,5 +1,5 @@
{
"name": "@deepseek-ai/dsh-tool-result-prune",
"name": "@deepseek-ai/dsh-compact-tool-result-prune",
"description": "Replay-safe model-free head/middle/tail pruning for tool-result surface nodes",
"version": "0.0.1",
"private": true,

View File

@@ -1,7 +1,7 @@
/**
* Replay-safe, model-free tool-result pruning service.
*
* @module @deepseek-ai/dsh-tool-result-prune
* @module @deepseek-ai/dsh-compact-tool-result-prune
*/
import { Context, Service } from 'cordis'

View File

@@ -6,7 +6,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import Include from '@cordisjs/plugin-include'
import ToolResultPruneService from '@deepseek-ai/dsh-tool-result-prune'
import ToolResultPruneService from '@deepseek-ai/dsh-compact-tool-result-prune'
let root: string | undefined
let context: Context | undefined
@@ -18,12 +18,12 @@ afterEach(async () => {
root = undefined
})
describe('tool-result-prune real Loader composition', () => {
describe('compact-tool-result-prune real Loader composition', () => {
it('loads and resolves the flat YAML plugin shape', async () => {
root = await mkdtemp(join(tmpdir(), 'dsh-tool-result-prune-loader-'))
root = await mkdtemp(join(tmpdir(), 'dsh-compact-tool-result-prune-loader-'))
const configPath = join(root, 'cordis.yml')
await writeFile(configPath, [
"- name: '@deepseek-ai/dsh-tool-result-prune'",
"- name: '@deepseek-ai/dsh-compact-tool-result-prune'",
' config:',
' thresholdChars: 100',
' headChars: 20',
@@ -38,7 +38,7 @@ describe('tool-result-prune real Loader composition', () => {
context.loader.internal = {
version: 'v2',
async import(specifier: string) {
if (specifier !== '@deepseek-ai/dsh-tool-result-prune') {
if (specifier !== '@deepseek-ai/dsh-compact-tool-result-prune') {
throw new Error(`unexpected Loader import: ${specifier}`)
}
return ToolResultPruneService

View File

@@ -10,8 +10,8 @@ import ToolResultPruneService, {
DEFAULTS,
PRUNE_MARKER,
resolveConfig,
} from '@deepseek-ai/dsh-tool-result-prune'
import type { ToolResultPruneConfig } from '@deepseek-ai/dsh-tool-result-prune'
} from '@deepseek-ai/dsh-compact-tool-result-prune'
import type { ToolResultPruneConfig } from '@deepseek-ai/dsh-compact-tool-result-prune'
const SMALL: ToolResultPruneConfig = {
thresholdChars: 50,

View File

@@ -72,5 +72,5 @@ Subclass `CompactService`, implement `compactIfNeeded` and `compactRegion`, and
## Known Limitations and Deferred Work
- **No model-facing consumer tier yet** — `@deepseek-ai/dsh-tool-compact` (the `/compact` tool) is deferred; compaction is reachable only via direct `ctx.compact` calls or a backend's auto listener.
- **Single-unit overflow is out of contract** — one indivisible unit (a closed tool pair or a large pasted `user/message`) alone exceeding the budget cannot be compacted.
- **Some single-unit overflow is out of contract** — balanced summary compaction cannot split one indivisible unit. The optional pruning companion can still repair a closed tool pair when text-bearing tool-result bulk is removable; a large non-tool node or a tool unit whose non-prunable remainder is oversized cannot be compacted.
- **An envelope that alone approaches the window is not surface-compaction work** — compaction shrinks derived history, never the system prompt, tools, or session prefix.