Merge remote-tracking branch 'origin/master' into worktree/provider-routed-llm-adapters
# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.md # docs/event-producer-consumer.md # docs/persistence-catalog.md # examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl # examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl # examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl # examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl # examples/acp-agent/tests/snapshots/skill-load/session.jsonl # examples/acp-agent/tests/snapshots/text-turn/session.jsonl # examples/sandbox-acp-agent/cordis.yml # examples/sandbox-acp-agent/tests/snapshots/escalation-approved/session.jsonl # examples/sandbox-acp-agent/tests/snapshots/escalation-rejected/session.jsonl # examples/sandbox-acp-agent/tests/snapshots/mode-switching/session.jsonl # packages/compact/compact-basic/README.md # packages/compact/compact-basic/src/index.ts # packages/compact/compact-basic/tests/compact-basic.spec.ts # packages/core/agent-loop/README.md # packages/core/agent-loop/src/loop.ts # packages/core/agent-loop/tests/properties.spec.ts # packages/core/session/README.md # packages/core/session/src/types.ts # packages/core/session/tests/derived-cache.spec.ts # packages/llm/llm-deepseek/src/index.ts # packages/llm/llm-pi-ai/README.md # packages/llm/llm-pi-ai/src/adapter.ts # packages/llm/llm-pi-ai/src/convert.ts # packages/llm/llm-pi-ai/tests/adapter.spec.ts # packages/llm/llm/README.md # packages/llm/llm/src/call-config.ts # packages/llm/llm/src/index.ts # packages/ui/acp-agent/src/index.ts # packages/ui/acp/tests/harness.ts # packages/ui/jsonrpc/README.md # packages/ui/jsonrpc/src/server.ts # packages/ui/stdio-agent/README.md # packages/ui/stdio-agent/src/index.ts # python/sdk/README.i18n.yaml
This commit is contained in:
@@ -7,7 +7,7 @@ This package is the interface tier of the compaction capability, split so each c
|
||||
| Package | Role |
|
||||
|---|---|
|
||||
| `@deepseek-ai/dsh-compact` (this) | the interface: abstract service + `compact/*` events + `CompactionResult` + the shared transcript renderer (`renderTranscript`/`renderContentBlocks`) |
|
||||
| `@deepseek-ai/dsh-compact-basic` (deferred) | a backend: chars-per-token estimation (`charsPerToken`, default 4) + token-budget retention + `llm.stream()` summarization |
|
||||
| `@deepseek-ai/dsh-compact-basic` | a backend: chars-per-token estimation (`charsPerToken`, default 4) + token-budget retention + `llm.stream()` summarization |
|
||||
| `@deepseek-ai/dsh-tool-compact` (deferred) | the model-facing `/compact` tool over `ctx.compact` |
|
||||
|
||||
Unlike the bash seam, this interface depends on `@deepseek-ai/dsh-session` and `@deepseek-ai/dsh-llm` — the contract's verbs are defined over a `Session` and its output is the `ContentBlock` vocabulary, so they cannot be expressed without naming those packages. That deviation from the "interface depends only on cordis" guidance is intentional and recorded in the [compaction capability-seam RFC](../../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md).
|
||||
@@ -48,3 +48,24 @@ The `compact/*` events extend `SessionEventMap` (merge-extensible) via declarati
|
||||
## Implementing a backend
|
||||
|
||||
Subclass `CompactService`, implement `compactIfNeeded` and `compactRegion`, and load the subclass as a plugin — it registers as `ctx.compact`. A tokenizer-, template-, or model-backed implementation can live as a sibling package without changing callers.
|
||||
|
||||
## Model Experience
|
||||
|
||||
### Conversation history, when a backend is invoked
|
||||
|
||||
**What the model sees**: A successful implementation replaces an older surface range with one user-role summary checkpoint; the raw events stay logged but stop appearing in derived model messages. The seam itself performs no rewrite.
|
||||
|
||||
**Token effect**: Zero direct tokens from this interface. A backend trades many retained history tokens for one summary and leaves the recent tail unchanged.
|
||||
|
||||
### Transcript supplied to a compaction consumer
|
||||
|
||||
**What the model sees**: `renderTranscript()` joins entries with one blank line and renders them exactly as `User: <content>`, `Assistant: <content>`, `Tool result (call <callId>): <content>`, `Tool error (call <callId>): <content>`, `[Context: <content>]`, or `[Steering: <content>]`. Non-text blocks render exactly as `[reasoning: <text>]`, `[tool-call: <name>(<arguments>)]`, `[tool-result: <content>]`, `[tool-result]`, or `[<block-type>]`.
|
||||
|
||||
**Token effect**: Data-dependent input tokens are paid only by the auxiliary model or consumer that requests this transcript; the conversation model does not receive a duplicate transcript.
|
||||
|
||||
## 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 retained unit (a closed step or a large pasted `user/message`) alone exceeding the budget cannot be compacted; the call may go out over-budget.
|
||||
- **A session prefix that alone approaches the window is a configuration error no backend fixes** — compaction shrinks derived history, never the prefix.
|
||||
- **Request context injected by downstream `agent/request` listeners sits outside pressure accounting** — `compactIfNeeded` counts prefix, derived history, and system prompt only.
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
/**
|
||||
* The compaction service seam (`ctx.compact`): an abstract service defining
|
||||
* WHAT compaction does — decide when to compact, summarize a range of
|
||||
* conversation history into a single surface node — without saying HOW.
|
||||
*
|
||||
* Implementations subclass {@link CompactService}, implement
|
||||
* {@link CompactService.compactIfNeeded} and {@link CompactService.compactRegion},
|
||||
* and load as a plugin — registering as `ctx.compact` (one implementation per
|
||||
* context). A tokenizer-, template-, or model-backed implementation can live
|
||||
* as a sibling package; callers stay on the same `ctx.compact` seam without
|
||||
* touching consumers.
|
||||
*
|
||||
* The split follows the capability-seams RFC — interface (this) /
|
||||
* implementation (deferred) / consumer (a `/compact` tool, deferred) — modeled
|
||||
* on the bash trio. Unlike `dsh-bash`, this interface necessarily
|
||||
* depends on `dsh-session` and `dsh-llm`: the contract's verbs are defined over
|
||||
* a `Session` and its output is the `ContentBlock` vocabulary. That deviation
|
||||
* from the "interface depends only on cordis" guidance is intentional and
|
||||
* recorded in the [compaction capability-seam RFC](../../../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md).
|
||||
*
|
||||
* Compaction service seam (`ctx.compact`): implementations decide when to
|
||||
* compact and replace a history range with one summary node by subclassing
|
||||
* {@link CompactService}. This interface necessarily depends on session and LLM
|
||||
* vocabulary; the rationale is in the
|
||||
* [compaction RFC](../../../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md).
|
||||
* @module @deepseek-ai/dsh-compact
|
||||
*/
|
||||
|
||||
@@ -42,25 +28,10 @@ declare module 'cordis' {
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract compaction service. Subclass implement the two abstract methods,
|
||||
* and load the subclass as a plugin — it registers as `ctx.compact` (one
|
||||
* implementation per context; loading a second throws, which is cordis'
|
||||
* standard duplicate-service behavior).
|
||||
*
|
||||
* Both core methods are abstract: the contract states WHAT compaction does,
|
||||
* while the entire strategy — token estimation, retention policy, event
|
||||
* sequencing, summarization — is a HOW decision owned by the implementation.
|
||||
*
|
||||
* Implementations MUST honor:
|
||||
* - **Surface contract**: a successful compaction shadows the compacted surface
|
||||
* nodes with a SINGLE replacement node carrying the summary. Because
|
||||
* `SurfaceEventType` is a closed union, that node is a `user/message` with
|
||||
* `surfaceOp: { op:'replace', start, end }`; the `compact/*` events are
|
||||
* log-only (lock + provenance).
|
||||
* - **Blocking**: no compaction begins while another is in progress for the
|
||||
* same session. The recommended mechanism is the log-recorded lock — append
|
||||
* `compact/start` before the slow work and `compact/end` after (even on
|
||||
* failure) — so the lock is visible to replay and crash recovery.
|
||||
* Abstract compaction service. Implementations own token estimation, retention,
|
||||
* and summarization, but a successful run must replace the selected surface span
|
||||
* with one summary node and prevent concurrent compaction of the same session.
|
||||
* Load one implementation per context as `ctx.compact`.
|
||||
*/
|
||||
export abstract class CompactService extends Service {
|
||||
constructor(ctx: Context) {
|
||||
@@ -69,43 +40,17 @@ export abstract class CompactService extends Service {
|
||||
|
||||
/**
|
||||
* Check token pressure and compact if the conversation is too large.
|
||||
*
|
||||
* Estimates the NEXT request's size — the session prefix, the
|
||||
* surface-derived history, and the system prompt — and if it exceeds the
|
||||
* backend's threshold, compacts an older range
|
||||
* via {@link compactRegion}, keeping recent context intact. Returns `null`
|
||||
* when no compaction is needed.
|
||||
*
|
||||
* Scope and guarantees a backend MUST honor:
|
||||
* - **Compaction acts on surface-derived history only**, but the ESTIMATE
|
||||
* counts everything the request carries: the loop composes the session
|
||||
* prefix before the pre-step seam fires and hands it here, so the gate
|
||||
* sees the prefix this instance will actually send (`EpochHeader.messagePrefix`
|
||||
* — request-only, never derived history). Non-surface context injected
|
||||
* downstream (into the request `messages` by a later listener) is out of
|
||||
* this accounting by construction.
|
||||
* - **Head-anchored, best-effort.** Auto-compaction consolidates from the
|
||||
* surface HEAD up to a balanced tool-pairing cutoff, so a prior head
|
||||
* checkpoint is
|
||||
* re-summarized into one fresh checkpoint (the surface holds at most one
|
||||
* auto-generated checkpoint, always at the head). It is best-effort over
|
||||
* CLOSED steps: when the only compactable content left is an un-splittable
|
||||
* open tail step, it declines (`null`) and retries once that step closes.
|
||||
* - **Single-unit overflow is out of scope.** If a single retained unit (one
|
||||
* closed step, or a large free node such as a 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 —
|
||||
* as is a session prefix that alone approaches the window (a
|
||||
* configuration error no compactor fixes: compaction cannot shrink the
|
||||
* prefix).
|
||||
* Estimate the next request, including its session prefix, derived history,
|
||||
* and system prompt. Above threshold, compact a head-anchored range ending at
|
||||
* a balanced tool boundary and reconsolidate any prior automatic checkpoint.
|
||||
* Return `null` when no compaction is needed or an open tail leaves no safe
|
||||
* cutoff. A single oversized retained unit or prefix cannot be repaired here.
|
||||
*
|
||||
* @param agent - agent context owning the session surface and model options.
|
||||
* @param fullSystemPrompt - assembled system prompt, counted toward the estimate.
|
||||
* @param sessionPrefix - the instance's composed session prefix, counted toward the estimate.
|
||||
* @param signal - cancellation signal. A backend summarizing via
|
||||
* `ctx.llm.stream()` MUST forward this into the call's `GenerateOptions.signal`
|
||||
* so an abort/dispose tears down the in-flight summarization rather than
|
||||
* leaving an orphaned model call running past the cancellation.
|
||||
* @param sessionPrefix - the instance's composed session prefix, counted toward the
|
||||
* estimate.
|
||||
* @param signal - cancellation signal; model-backed implementations must forward it.
|
||||
* @returns the compaction result, or `null` if no compaction was needed.
|
||||
*/
|
||||
abstract compactIfNeeded(
|
||||
@@ -117,36 +62,19 @@ export abstract class CompactService extends Service {
|
||||
|
||||
/**
|
||||
* Forcibly compact a range of surface nodes into a single summary node.
|
||||
* `start` and `end` name an inclusive span by surface position, not numeric seq
|
||||
* order; replacements can make visible seqs non-monotonic. Both edges must be
|
||||
* balanced so assistant tool calls remain paired with their results. A model-
|
||||
* backed implementation forwards cancellation and rejects active, missing,
|
||||
* reversed, or unbalanced ranges.
|
||||
*
|
||||
* `start` and `end` are inclusive seqs of surface nodes to shadow; the backend
|
||||
* summarizes their content and appends a replacement surface node. Used by the
|
||||
* (future) `/compact` tool and internally by {@link compactIfNeeded}.
|
||||
*
|
||||
* The region MUST NOT split a step's `assistant/message` tool-calls from their
|
||||
* `tool/result`s, leaving the rehydrated transcript with a dangling tool-call
|
||||
* or an orphaned tool-result that every provider rejects. A region is safe iff
|
||||
* both its edges are balanced cuts on the surface: the cut before `start` and
|
||||
* the cut after `end` each have no unanswered tool-call before them. A node
|
||||
* that belongs to no step (a pre-step user message, inter-step steering, or an
|
||||
* injection context message) is a balanced (free) boundary; an `end` inside an
|
||||
* open (unclosed) tail step is invalid — its tool-calls have no results yet.
|
||||
* `dsh-session` exports `isToolPairingBalanced` for this check.
|
||||
*
|
||||
* @param session - the session whose surface is mutated.
|
||||
* @param start - inclusive seq of the first surface node to compact.
|
||||
* @param end - inclusive seq of the last surface node to compact.
|
||||
* @param agent - agent context used by router-aware summarizers.
|
||||
* @param signal - optional cancellation signal. A backend that summarizes via
|
||||
* `ctx.llm.stream()` MUST forward this into the call's `GenerateOptions.signal`
|
||||
* so an abort/dispose tears down the in-flight summarization rather than
|
||||
* leaving an orphaned model call running past the cancellation.
|
||||
* @throws if compaction is already in progress, if `start`/`end` are not
|
||||
* valid surface nodes, if `start` is positioned after `end` on the surface
|
||||
* (the range is a surface-POSITION span, not a numeric seq interval — a
|
||||
* prior replace can leave the surface non-monotonic in seq order), or if
|
||||
* either boundary is not a balanced tool-pairing cut (would split a step's
|
||||
* tool-call/result pair).
|
||||
* @returns what the compaction did (the replaced range and its summary node).
|
||||
* @param session - session to mutate.
|
||||
* @param start - first surface seq, inclusive.
|
||||
* @param end - last surface seq, inclusive.
|
||||
* @param agent - summarizer context.
|
||||
* @param signal - optional cancellation; model-backed implementations must forward it.
|
||||
* @throws when compaction is active or the range is missing, reversed, or unbalanced.
|
||||
* @returns the replaced range and summary.
|
||||
*/
|
||||
abstract compactRegion(
|
||||
session: Session,
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
/**
|
||||
* Plain-text transcript rendering over session events: the shared projection
|
||||
* used wherever a compaction-class consumer needs "what a model once saw" as
|
||||
* readable text — a summarizer's input, or a recall tool's output.
|
||||
*
|
||||
* Extracted from the basic backend's private helpers so the summarize path and
|
||||
* the recall read path render one span identically (two renderers would drift,
|
||||
* and a recall reader would then see a different transcript than the one the
|
||||
* summary was written from). Both functions are pure over their arguments: no
|
||||
* session access beyond the provided events, no clock, no randomness — a
|
||||
* rendered span is a pure function of the log, so replay reproduces it
|
||||
* byte-identically.
|
||||
*
|
||||
* Pure shared transcript projection for summarization and recall, so both
|
||||
* render the same log span byte-for-byte under replay.
|
||||
* @module @deepseek-ai/dsh-compact/render
|
||||
*/
|
||||
|
||||
@@ -18,14 +8,9 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
|
||||
/**
|
||||
* Render content blocks to a single plain-text string. Text and reasoning
|
||||
* contribute their text (reasoning wrapped as `[reasoning: …]`); every other
|
||||
* block type contributes a type-tagged placeholder (`[tool-call: name(args)]`,
|
||||
* `[tool-result: …]`, …) so the reader is told what non-text content existed
|
||||
* rather than silently losing it. A `tool-result` block recurses into its
|
||||
* nested content (`[tool-result: <inner rendering>]`), falling back to a bare
|
||||
* `[tool-result]` when the nested content renders to nothing. Blocks join
|
||||
* with newlines; empty-text blocks contribute nothing.
|
||||
* Render text directly, reasoning as a tagged span, and every other block as a
|
||||
* type-tagged placeholder. Tool results recurse into nested content; empty
|
||||
* blocks contribute nothing and rendered blocks join with newlines.
|
||||
*
|
||||
* @param blocks - the content blocks to render.
|
||||
* @returns the newline-joined plain-text rendering; empty string when nothing renders.
|
||||
@@ -59,17 +44,9 @@ export function renderContentBlocks(blocks: readonly ContentBlock[]): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a set of surface-node seqs as a `User:`/`Assistant:`/`Tool result:`
|
||||
* transcript. Walks `seqs` in the order given — callers pass surface order
|
||||
* (e.g. a `compactRegion` slice of the surface-node list), which after a
|
||||
* `replace` is NOT ascending log-seq order (a high-seq summary node can sit at
|
||||
* the head of the surface before older retained lower-seq nodes); a log-order
|
||||
* scan would render the transcript out of order.
|
||||
*
|
||||
* Only the five surface (message-producing) event types render; a seq naming
|
||||
* any other event type contributes nothing. `SessionEventMap` is
|
||||
* merge-extensible, so unknown types are simply non-message events with no
|
||||
* renderable text.
|
||||
* Render message-producing events as a role-labeled transcript. `seqs` are
|
||||
* walked in caller-supplied surface order, which may differ from numeric log
|
||||
* order after replacement; non-surface and unknown merged events are skipped.
|
||||
*
|
||||
* @param events - the session log the seqs index into (`session.events`).
|
||||
* @param seqs - the surface-node seqs to render, in surface order.
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
/**
|
||||
* Compaction vocabulary: the result type and the `compact/*` session events.
|
||||
*
|
||||
* Extends {@link SessionEventMap} with `compact/*` event types via declaration
|
||||
* merging. {@link SurfaceEventType} is deliberately NOT extended — `compact/*`
|
||||
* events are log-only markers (lock + provenance); only the five
|
||||
* surface-eligible types can carry `surfaceOp`. The actual surface mutation is
|
||||
* performed by a separate `user/message` event carrying the summary (see the
|
||||
* [compaction capability-seam RFC](../../../../docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md)).
|
||||
*
|
||||
* Configuration lives in the backend, not here: the contract states WHAT
|
||||
* compaction produces, while every tunable (context window, thresholds,
|
||||
* retention budget) is a HOW decision owned by the implementation.
|
||||
*
|
||||
* Those declaration-merged events are log-only lock/provenance markers, not
|
||||
* surface events; a separate replacement `user/message` carries the summary.
|
||||
* Backend packages own configuration and retention policy; see
|
||||
* `docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md`.
|
||||
* @module @deepseek-ai/dsh-compact/types
|
||||
*/
|
||||
|
||||
|
||||
Reference in New Issue
Block a user