diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index e384bf3ad0..e0f07cfce7 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -105,7 +105,7 @@ Abstract compaction service. Implementations own trigger policy, retention, and ```ts cordis-catalog abstract compactIfNeeded( agent: CompactAgentContext, fullSystemPrompt: string, sessionPrefix: readonly Message[], signal: AbortSignal, ): Promise -abstract compactRegion( session: Session, start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise +abstract compactRegion( start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise ``` Types: [Message](../core-data-structures/core.md) diff --git a/docs/core-data-structures/compaction.md b/docs/core-data-structures/compaction.md index 45a0ac310e..11f895b2a8 100644 --- a/docs/core-data-structures/compaction.md +++ b/docs/core-data-structures/compaction.md @@ -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](../../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)). +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 act on an agent-owned `Session`, and its durable summary event uses 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) @@ -20,7 +20,7 @@ These variants are merged inside a `declare module '@deepseek-ai/dsh-session'` b ## `CompactionResult` -What a successful compaction returns to its caller: the seqs of the three appended `compact/*` events, the summary blocks, and the shadowed range/seqs plus the estimated token count. +What a successful compaction returns to its caller: the bookkeeping-event seqs, raw summary, shadowed range and seqs, and estimated token count. ```ts type-equiv interface CompactionResult { diff --git a/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md b/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md index 864428bc58..1986ad7624 100644 --- a/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md +++ b/docs/rfc/implemented/feature/2026-06-18-compaction-capability-seam.md @@ -22,7 +22,7 @@ Per the [capability-seams RFC](../../implemented/architecture/2026-06-13-capabil ### The contract depends on `dsh-session` and `dsh-llm` — a deliberate deviation -The capability-seams RFC states the interface package "depends only on cordis" (true of `dsh-bash`, whose vocabulary is self-contained). Compaction **cannot** honor that: its verbs are defined *over* a `Session` (`compactRegion(session, start, end)`) and its output *is* the content vocabulary (`CompactionResult.summary: ContentBlock[]`). There is no way to express the contract without naming `Session`/`SessionEvent` (from `dsh-session`) and `ContentBlock` (from `dsh-llm`). +The capability-seams RFC states the interface package "depends only on cordis" (true of `dsh-bash`, whose vocabulary is self-contained). Compaction **cannot** honor that: its verbs act on an agent-owned `Session` (`compactRegion(start, end, agent)`) and its output uses the content vocabulary (`CompactionResult.summary: ContentBlock[]`). There is no way to express the contract without naming `Session`/`SessionEvent` (from `dsh-session`) and `ContentBlock` (from `dsh-llm`). This is not a coupling smell — it is the contract's domain. The "only cordis" guidance was always shorthand for "the interface depends only on what the contract genuinely names, and never on an implementation." `dsh-session` and `dsh-llm` are themselves interface/vocabulary packages, not implementations; `dsh-compact` still imports no backend. The seam's real invariant — *consumers and implementations evolve independently behind an abstract service* — holds intact. @@ -30,7 +30,7 @@ This is not a coupling smell — it is the contract's domain. The "only cordis" An earlier draft put the full algorithm (the retention walk, token-summing, text extraction) as concrete methods on the interface. That recouples the contract to one strategy: a backend that wants a different retention policy or event sequence would have to fight inherited concrete code. Making both core methods abstract puts every *how* decision in the backend and keeps the interface a statement of *what*. Token measurement is not a compaction hook at all; the singleton service lets multiple consumers share one per-session replay fold. -`compactIfNeeded(agent, fullSystemPrompt, sessionPrefix, signal)` takes required pressure inputs and cancellation. The session comes from the agent. `compactRegion(session, start, end, agent, signal?)` keeps an optional signal for manual callers and requires `session === agent.session`; implementations reject mismatch before model resolution, lock acquisition, summarization, or log mutation. The pre-step integration resolves a provisional model from the latest logged request header, then `AgentOptions.model`; a model-less router-only first step skips pressure because `agent/request` can route later. The default summarizer resolves its model from explicit config, the latest logged routed model, then agent options. +`compactIfNeeded(agent, fullSystemPrompt, sessionPrefix, signal)` takes required pressure inputs and cancellation. `compactRegion(start, end, agent, signal?)` uses `agent.session` as its single session identity and keeps an optional signal for manual callers. The pre-step integration resolves a provisional provider/model pair from the latest logged request header, then `AgentOptions`; a model-less router-only first step skips pressure because `agent/request` can route later. The default summarizer resolves its target from explicit config, the latest logged routed target, then agent options. ### Auto-compaction runs on `agent/pre-step`, a dedicated surface-mutation seam diff --git a/packages/compact/compact-basic/README.md b/packages/compact/compact-basic/README.md index 937120eebe..950e815ebc 100644 --- a/packages/compact/compact-basic/README.md +++ b/packages/compact/compact-basic/README.md @@ -13,10 +13,10 @@ This backend owns the compaction policy: - **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 provider/model pair and cap, falling back to the latest logged request target and then the agent target, 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 `` tags. The raw summary remains on the provenance event, and later automatic cycles merge the prior checkpoint. -- **Lifecycle** — `compactRegion()` requires its agent to own the exact target session and rejects mismatch before resolution or mutation; a valid call records its start, summary, replacement, and end. The serial `agent/pre-step` listener checks pressure before every step, outside an open step, so a tool-heavy turn remains compactable and the loop derives history once after mutation. +- **Lifecycle** — `compactRegion()` mutates `agent.session` and records its start, summary, replacement, and end. The serial `agent/pre-step` listener checks pressure before every step, outside an open step, so a tool-heavy turn remains compactable and the loop derives history once after mutation. - **Failure handling** — an unmatched `compact/start` is an inert crash marker because no replacement landed. Recoverable failure records an error end and leaves the surface unchanged. -`summarize()` is the sole subclass hook. A template- or remote-summarizer subclass can override it while pressure, retention, provenance, shrink validation, and shadowed-token accounting stay on `ctx.tokenMeter`. The hook returns the summary blocks together with the call envelope it used (`{ summary, provider, model, maxTokens? }`), which is logged on `compact/summary`. +The protected `summarize()` method is the sole subclass hook. A template- or remote-summarizer subclass can override it while pressure, retention, provenance, shrink validation, and shadowed-token accounting stay on `ctx.tokenMeter`. The hook returns the summary blocks together with the call envelope it used (`{ summary, provider, model, maxTokens? }`), which is logged on `compact/summary`. ## Config (`BasicCompactConfig`) diff --git a/packages/compact/compact-basic/src/index.ts b/packages/compact/compact-basic/src/index.ts index 60452581c5..6912f0a0a4 100644 --- a/packages/compact/compact-basic/src/index.ts +++ b/packages/compact/compact-basic/src/index.ts @@ -21,7 +21,6 @@ import type { ResolvedConfig, } from './types.ts' -export { resolveConfig } from './config.ts' export type { BasicCompactConfig, ResolvedConfig, @@ -96,7 +95,7 @@ export class BasicCompactService extends CompactService { * @param signal - optional cancellation forwarded to the adapter. * @returns safe text summary blocks and exact auxiliary-call provenance. */ - async summarize( + protected async summarize( text: string, agent: Agent, signal?: AbortSignal, @@ -137,7 +136,7 @@ export class BasicCompactService extends CompactService { /* v8 ignore next -- paired with the defensive post-success branch above. */ break } - result = await this.compactRegion(agent.session, range.start, range.end, agent, signal) + result = await this.compactRegion(range.start, range.end, agent, signal) measurement = meter.measure(agent.session, requestHeader) if (measurement.totalTokens < threshold) return result } @@ -149,10 +148,8 @@ export class BasicCompactService extends CompactService { } /** - * Compact one inclusive positional surface range using the effective - * token meter for all retention and shrink pricing. Reject an agent that does - * not own the exact target before any mutation. - * @param session - session whose surface is mutated; must equal `agent.session`. + * Compact one inclusive positional range from the agent-owned surface using + * the effective token meter for all retention and shrink pricing. * @param start - inclusive first surface-node seq. * @param end - inclusive last surface-node seq. * @param agent - owner of the target session, used by the summarizer. @@ -160,15 +157,12 @@ export class BasicCompactService extends CompactService { * @returns the successful durable compaction result. */ override async compactRegion( - session: Session, start: number, end: number, agent: Agent, signal?: AbortSignal, ): Promise { - if (session !== agent.session) { - throw new Error('compactRegion: agent.session must be the exact target session') - } + const session = agent.session return compactSurfaceRegion({ meter: this.ctx.tokenMeter, summarize: (text, owner, abort) => this.summarize(text, owner, abort), diff --git a/packages/compact/compact-basic/tests/compact-basic.spec.ts b/packages/compact/compact-basic/tests/compact-basic.spec.ts index 2d315293ed..caaac783b6 100644 --- a/packages/compact/compact-basic/tests/compact-basic.spec.ts +++ b/packages/compact/compact-basic/tests/compact-basic.spec.ts @@ -1,8 +1,9 @@ import { describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' -import BasicCompactService, { resolveConfig } from '@deepseek-ai/dsh-compact-basic' +import BasicCompactService from '@deepseek-ai/dsh-compact-basic' import type { BasicCompactConfig } from '@deepseek-ai/dsh-compact-basic' import { selectCompactableRange } from '@deepseek-ai/dsh-compact-basic/src/region.ts' +import { resolveConfig } from '@deepseek-ai/dsh-compact-basic/src/config.ts' import type { CompactionResult } from '@deepseek-ai/dsh-compact' import LlmService, { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' import type { ContentBlock, GenerateOptions, Message, StreamChunk } from '@deepseek-ai/dsh-llm' @@ -349,32 +350,11 @@ describe('pressure measurement and retention', () => { }) describe('compaction region transaction', () => { - it('rejects an agent that does not own the exact target session before mutation', async () => { - const compact = service() - const target = conversation(2) - const owner = conversation(1) - const targetEvents = [...target.events] - const ownerEvents = [...owner.events] - const nodes = target.surface.nodes - - await expect(compact.compactRegion( - target, - nodes[0]!, - nodes[1]!, - agent(owner), - )).rejects.toThrow('compactRegion: agent.session must be the exact target session') - - expect(target.events).toEqual(targetEvents) - expect(owner.events).toEqual(ownerEvents) - expect(compact.calls).toEqual([]) - }) - it('lands a framed, replayable checkpoint with exact pricing provenance', async () => { const compact = service() const session = conversation(3) const before = session.surface.nodes const result = await compact.compactRegion( - session, before[0]!, before[3]!, agent(session, MODEL), @@ -410,7 +390,6 @@ describe('compaction region transaction', () => { const session = conversation(2) const nodes = session.surface.nodes await expect(compact.compactRegion( - session, startOverride ?? nodes[0]!, endOverride ?? nodes[1]!, agent(session, MODEL), @@ -422,7 +401,6 @@ describe('compaction region transaction', () => { const plain = conversation(2) const nodes = plain.surface.nodes await expect(compact.compactRegion( - plain, nodes[2]!, nodes[1]!, agent(plain, MODEL), @@ -431,13 +409,11 @@ describe('compaction region transaction', () => { const tools = toolConversation() const toolNodes = tools.surface.nodes await expect(compact.compactRegion( - tools, toolNodes[2]!, toolNodes[4]!, agent(tools, MODEL), )).rejects.toThrow(/start seq .* not a balanced boundary/) await expect(compact.compactRegion( - tools, toolNodes[0]!, toolNodes[1]!, agent(tools, MODEL), @@ -450,7 +426,6 @@ describe('compaction region transaction', () => { closed.append('turn/end', { turn: 2, reason: { kind: 'completed' } }) const nodes = closed.surface.nodes await expect(compact.compactRegion( - closed, nodes[0]!, nodes[1]!, agent(closed, MODEL), @@ -460,7 +435,6 @@ describe('compaction region transaction', () => { locked.append('compact/start', { turn: 2 }) const lockedNodes = locked.surface.nodes await expect(compact.compactRegion( - locked, lockedNodes[0]!, lockedNodes[1]!, agent(locked, MODEL), @@ -477,7 +451,6 @@ describe('compaction region transaction', () => { const node = session.surface.nodes[0]! await expect(compact.compactRegion( - session, node, node, agent(session, MODEL), @@ -497,7 +470,6 @@ describe('compaction region transaction', () => { const nodes = session.surface.nodes await expect(compact.compactRegion( - session, nodes[0]!, nodes[2]!, agent(session, MODEL), @@ -511,7 +483,6 @@ describe('compaction region transaction', () => { const before = session.surface.nodes await expect(compact.compactRegion( - session, before[0]!, before[2]!, agent(session, MODEL), @@ -527,7 +498,6 @@ describe('compaction region transaction', () => { const session = conversation(2) const nodes = session.surface.nodes await expect(compact.compactRegion( - session, nodes[0]!, nodes[2]!, agent(session, MODEL), @@ -548,7 +518,6 @@ describe('compaction region transaction', () => { const nodes = session.surface.nodes await expect(compact.compactRegion( - session, nodes[0]!, nodes[2]!, agent(session, MODEL), @@ -566,7 +535,6 @@ describe('compaction region transaction', () => { const nodes = session.surface.nodes await expect(compact.compactRegion( - session, nodes[0]!, nodes[2]!, agent(session, MODEL), @@ -579,7 +547,6 @@ describe('compaction region transaction', () => { const session = conversation(1) const nodes = session.surface.nodes await expect(compact.compactRegion( - session, nodes[0]!, nodes[1]!, agent(session), @@ -613,18 +580,28 @@ class ScriptedAdapter extends LlmAdapter { } } +class ExposedCompactService extends BasicCompactService { + runSummarize( + text: string, + owner: Agent, + signal?: AbortSignal, + ): Promise<{ summary: ContentBlock[]; provider: string; model: string; maxTokens?: number }> { + return this.summarize(text, owner, signal) + } +} + async function summarizerHarness( blocks: readonly ContentBlock[], finish?: (StreamChunk & { type: 'finish' })['reason'], model = MODEL, config: BasicCompactConfig = { auto: false }, -): Promise<{ ctx: Context; adapter: ScriptedAdapter; compact: BasicCompactService }> { +): Promise<{ ctx: Context; adapter: ScriptedAdapter; compact: ExposedCompactService }> { const ctx = new Context() await ctx.plugin(LlmService) void new TokenMeterService(ctx, { contextWindow: 1_000 }) const adapter = new ScriptedAdapter(blocks, finish) ctx.llm.registerAdapter([model], adapter) - const compact = new BasicCompactService(ctx, config) + const compact = new ExposedCompactService(ctx, config) return { ctx, adapter, compact } } @@ -641,7 +618,7 @@ describe('default one-shot summarizer', () => { maxTokens: 321, }) const session = conversation(1) - const output = await compact.summarize('transcript', agent(session, 'fallback'), SIGNAL) + const output = await compact.runSummarize('transcript', agent(session, 'fallback'), SIGNAL) expect(output).toEqual({ summary: [{ type: 'text', text: 'public summary' }], @@ -666,7 +643,7 @@ describe('default one-shot summarizer', () => { header: { config: { provider: 'routed', model: 'routed' } }, reason: 'initial', }) - const output = await compact.summarize('history', agent(session, 'fallback')) + const output = await compact.runSummarize('history', agent(session, 'fallback')) expect(output.provider).toBe('routed') expect(output.model).toBe('routed') expect(adapter.lastOptions?.provider).toBe('routed') @@ -677,8 +654,8 @@ describe('default one-shot summarizer', () => { const ctx = new Context() await ctx.plugin(LlmService) void new TokenMeterService(ctx) - const compact = new BasicCompactService(ctx, { auto: false }) - await expect(compact.summarize('history', agent(new Session(SessionId('model-less'))))) + const compact = new ExposedCompactService(ctx, { auto: false }) + await expect(compact.runSummarize('history', agent(new Session(SessionId('model-less'))))) .rejects.toThrow(/no provider\/model available for summarization/) }) @@ -693,7 +670,7 @@ describe('default one-shot summarizer', () => { const { compact } = await summarizerHarness([], finish) let thrown: unknown try { - await compact.summarize('history', agent(conversation(1), MODEL)) + await compact.runSummarize('history', agent(conversation(1), MODEL)) } catch (error: unknown) { thrown = error } @@ -705,7 +682,7 @@ describe('default one-shot summarizer', () => { it('rejects empty or reasoning-only successful output', async () => { const { compact } = await summarizerHarness([{ type: 'reasoning', text: 'private' }]) - await expect(compact.summarize('history', agent(conversation(1), MODEL))) + await expect(compact.runSummarize('history', agent(conversation(1), MODEL))) .rejects.toThrow(/no text summary content/) }) }) diff --git a/packages/compact/compact/README.md b/packages/compact/compact/README.md index ec14947090..80ddc00e1a 100644 --- a/packages/compact/compact/README.md +++ b/packages/compact/compact/README.md @@ -19,7 +19,9 @@ Both methods are **abstract** — the backend owns trigger policy, retention, ev | Member | Semantics | |---|---| | `compactIfNeeded(agent, fullSystemPrompt, sessionPrefix, signal)` | Estimate the surface-derived history size; if over the backend's threshold, compact an older range via `compactRegion`, keeping recent context intact. Returns the `CompactionResult`, or `null` if nothing needed compacting. All parameters required — the loop's `agent/pre-step` checkpoint supplies the agent, assembled `fullSystemPrompt`, composed `sessionPrefix` (request-only messages every request carries but the derived history omits — the pressure estimate must count them), and turn `signal`. A backend's summarization request is a direct `ctx.llm.stream()` call (not a loop step), so per-call interception happens at `llm/stream`. | -| `compactRegion(session, start, end, agent, signal?)` | Forcibly summarize surface nodes `[start, end]` (inclusive seqs) into a single replacement node. The agent must own the exact target (`session === agent.session`); a backend rejects mismatch before model resolution, lock acquisition, summarization, or log mutation. **Throws** if a compaction is already in progress, if `start`/`end` aren't surface nodes, or if `start` is positioned after `end` on the surface. The range is a SURFACE-POSITION span, not a numeric seq interval — after a prior replace lands a fresh high-seq summary node at the shadowed range's position, surface order no longer tracks seq order. | +| `compactRegion(start, end, agent, signal?)` | Forcibly summarize surface nodes `[start, end]` (inclusive seqs) from `agent.session` into a single replacement node. **Throws** if a compaction is already in progress, if `start`/`end` aren't surface nodes, or if `start` is positioned after `end` on the surface. The range is a SURFACE-POSITION span, not a numeric seq interval — after a prior replace lands a fresh high-seq summary node at the shadowed range's position, surface order no longer tracks seq order. | + +`CompactionResult` keeps the raw summary and bookkeeping-event seqs available to callers alongside the shadowed range and token accounting; its drift-checked shape lives in the [compaction data-structure reference](../../../docs/core-data-structures/compaction.md#compactionresult). `compactIfNeeded` takes a required `signal`; `compactRegion`'s is optional. A backend that summarizes via `ctx.llm.stream()` **must** forward it into the call's `GenerateOptions.signal`, so an abort or fiber dispose tears down the in-flight summarization instead of leaving an orphaned model call running past the cancellation. The turn that the `compact/*` events belong to is recoverable from the owned session's log (the currently-open turn), so the backend stamps it from the log rather than trusting a caller-supplied value. diff --git a/packages/compact/compact/src/index.ts b/packages/compact/compact/src/index.ts index 7361d38ef3..8ec4f59d96 100644 --- a/packages/compact/compact/src/index.ts +++ b/packages/compact/compact/src/index.ts @@ -67,23 +67,19 @@ export abstract class CompactService extends Service { * `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. The agent must own the exact - * target session object; implementations reject an ownership mismatch before - * model resolution, lock acquisition, summarization, or log mutation, and - * reject active, missing, reversed, or unbalanced ranges. + * backed implementation forwards cancellation and rejects active, missing, + * reversed, or unbalanced ranges. The target session is `agent.session`. * Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter} * for the edge checks. * - * @param session - session to mutate; must be identical to `agent.session`. * @param start - first surface seq, inclusive. * @param end - last surface seq, inclusive. - * @param agent - owner of the target session and summarizer context. + * @param agent - context whose session is mutated and whose routing options guide summarization. * @param signal - optional cancellation; model-backed implementations must forward it. - * @throws when the agent does not own `session`, compaction is active, or the range is missing, reversed, or unbalanced. - * @returns the replaced range and summary. + * @throws when compaction is active or the range is missing, reversed, or unbalanced. + * @returns the appended event seqs, summary, replaced range, and token accounting. */ abstract compactRegion( - session: Session, start: number, end: number, agent: CompactAgentContext, diff --git a/packages/compact/compact/tests/compact.spec.ts b/packages/compact/compact/tests/compact.spec.ts index f272b1fe85..d99510baa6 100644 --- a/packages/compact/compact/tests/compact.spec.ts +++ b/packages/compact/compact/tests/compact.spec.ts @@ -27,17 +27,18 @@ class StubCompactService extends CompactService { } override async compactRegion( - session: Session, start: number, end: number, - _agent: CompactAgentContext, + agent: CompactAgentContext, signal?: AbortSignal, ): Promise { this.lastSignal = signal + const session = agent.session + const summary = [{ type: 'text' as const, text: 'stub' }] // Minimal stub honoring the lock + log-only event contract. const startEvent = session.append('compact/start', { turn: 0 }) const summaryEvent = session.append('compact/summary', { - summary: [{ type: 'text', text: 'stub' }], + summary, shadowedRange: { start, end }, shadowedSeqs: [], shadowedTokenCount: 0, @@ -49,7 +50,7 @@ class StubCompactService extends CompactService { startSeq: startEvent.seq, summarySeq: summaryEvent.seq, endSeq: endEvent.seq, - summary: [{ type: 'text', text: 'stub' }], + summary, shadowedRange: { start, end }, shadowedSeqs: [], shadowedTokenCount: 0, @@ -89,7 +90,7 @@ describe('CompactService seam', () => { const svc = new StubCompactService(ctx) const session = new Session(SessionId('s')) - const result = await svc.compactRegion(session, 0, 0, stubAgent(session, 'm')) + const result = await svc.compactRegion(0, 0, stubAgent(session, 'm')) const startEvent = session.events.find(e => e.type === 'compact/start') expect(startEvent).toBeDefined() @@ -97,8 +98,12 @@ describe('CompactService seam', () => { // verify the runtime value is absent. const raw = startEvent as unknown as { surfaceOp?: unknown } expect(raw.surfaceOp).toBeUndefined() + expect(result.summary).toEqual([{ type: 'text', text: 'stub' }]) expect(result.summarySeq).toBeGreaterThan(result.startSeq) expect(result.endSeq).toBeGreaterThan(result.summarySeq) + expect(result.shadowedRange).toEqual({ start: 0, end: 0 }) + expect(session.events.filter(e => e.type.startsWith('compact/')).map(e => e.type)) + .toEqual(['compact/start', 'compact/summary', 'compact/end']) }) it('threads the cancellation signal through to the backend', async () => { @@ -107,7 +112,7 @@ describe('CompactService seam', () => { const session = new Session(SessionId('s')) const controller = new AbortController() - await svc.compactRegion(session, 0, 0, stubAgent(session, 'm'), controller.signal) + await svc.compactRegion(0, 0, stubAgent(session, 'm'), controller.signal) expect(svc.lastSignal).toBe(controller.signal) await svc.compactIfNeeded(stubAgent(session), '', [], controller.signal) diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 07b6228d62..96fe378def 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -112,7 +112,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ summary: 'Abstract compaction service.', methods: [ 'abstract compactIfNeeded( agent: CompactAgentContext, fullSystemPrompt: string, sessionPrefix: readonly Message[], signal: AbortSignal, ): Promise', - 'abstract compactRegion( session: Session, start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise', + 'abstract compactRegion( start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise', ], }, { diff --git a/website/zh-CN/api/harness/compact.md b/website/zh-CN/api/harness/compact.md index 8be3b4287a..67f17cc941 100644 --- a/website/zh-CN/api/harness/compact.md +++ b/website/zh-CN/api/harness/compact.md @@ -25,20 +25,19 @@ Check token pressure and compact if the conversation is too large. Estimate the [Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L58) -### ctx.compact.compactRegion(session, start, end, agent, signal?) +### ctx.compact.compactRegion(start, end, agent, signal?) ```ts website-api -abstract compactRegion( session: Session, start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise +abstract compactRegion( start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise ``` -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. The agent must own the exact target session object; implementations reject an ownership mismatch before model resolution, lock acquisition, summarization, or log mutation, and reject active, missing, reversed, or unbalanced ranges. Use toolPairingBalancedBefore and toolPairingBalancedAfter for the edge checks. +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. The target session is `agent.session`. Use toolPairingBalancedBefore and toolPairingBalancedAfter for the edge checks. -- `session` — session to mutate; must be identical to `agent.session`. - `start` — first surface seq, inclusive. - `end` — last surface seq, inclusive. -- `agent` — owner of the target session and summarizer context. +- `agent` — context whose session is mutated and whose routing options guide summarization. - `signal` — optional cancellation; model-backed implementations must forward it. -**Returns** the replaced range and summary. +**Returns** the appended event seqs, summary, replaced range, and token accounting. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L85) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/compact/compact/src/index.ts#L82)