From 66e5f8eecd00363d67c4dcbd6bc326daf789f1f2 Mon Sep 17 00:00:00 2001 From: Jinhua Zhu Date: Wed, 22 Jul 2026 18:35:32 +0800 Subject: [PATCH 1/3] feat(llm): mark compaction requests with x-deepseek-harness-compact header --- packages/compact/compact-basic/README.md | 2 +- packages/compact/compact-basic/src/summarizer.ts | 1 + .../compact-basic/tests/compact-basic.spec.ts | 1 + packages/llm/llm-deepseek/README.md | 2 +- packages/llm/llm-deepseek/src/adapter.ts | 3 +++ packages/llm/llm-deepseek/tests/adapter.spec.ts | 15 +++++++++++++++ packages/llm/llm/src/types.ts | 7 +++++++ 7 files changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/compact/compact-basic/README.md b/packages/compact/compact-basic/README.md index 94a9857397..a9374b9cfd 100644 --- a/packages/compact/compact-basic/README.md +++ b/packages/compact/compact-basic/README.md @@ -13,7 +13,7 @@ This backend owns the compaction policy: - **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 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 call replays the conversation's own system prompt, tools, and shadowed-region messages verbatim and appends the compaction instruction as the final user message, so it reuses the provider's warm prefix cache instead of invalidating it. Only returned text enters the checkpoint, excluding reasoning and tool calls that would leak private reasoning or create an orphaned call. +- **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 call replays the conversation's own system prompt, tools, and shadowed-region messages verbatim and appends the compaction instruction as the final user message, so it reuses the provider's warm prefix cache instead of invalidating it. It sets `GenerateOptions.compact`, which adapters may forward as request attribution (the DeepSeek adapter sends `x-deepseek-harness-compact: 1`) without touching the model-visible body. 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()` mutates `agent.session` and records its start, summary, replacement, and end. After asynchronous summarization it rejects a changed surface-node snapshot, while unrelated log-only events may append without invalidating the selected span. The serial `agent/post-step` listener checks pressure after successful output and tool work are durable but before `step/end`. Canonical provider overflow is handled through `agent/request-error` after the failed step closes. - **Overflow recovery** — provider-confirmed overflow needs no capacity metadata: it bypasses normal pressure and retention, prunes, then attempts one maximal balanced head reduction while leaving the newest indivisible unit. Retry is authorized whenever `surface.replaceGeneration` advances, including when pruning lands before later summary work throws. No replacement, an exhausted target-specific cap, cancellation, or an unknown/noncanonical error preserves the original provider failure. diff --git a/packages/compact/compact-basic/src/summarizer.ts b/packages/compact/compact-basic/src/summarizer.ts index cf34c2ad91..51adc19c60 100644 --- a/packages/compact/compact-basic/src/summarizer.ts +++ b/packages/compact/compact-basic/src/summarizer.ts @@ -138,6 +138,7 @@ export async function summarizeWithLlm( ...input.tools === undefined ? {} : { tools: [...input.tools] }, maxTokens: config.maxTokens, sessionId: agent.session.id, + compact: true, ...signal === undefined ? {} : { signal }, } for await (const chunk of ctx.llm.stream(options)) assembler.push(chunk) diff --git a/packages/compact/compact-basic/tests/compact-basic.spec.ts b/packages/compact/compact-basic/tests/compact-basic.spec.ts index f4e451dcc3..97d0127465 100644 --- a/packages/compact/compact-basic/tests/compact-basic.spec.ts +++ b/packages/compact/compact-basic/tests/compact-basic.spec.ts @@ -1094,6 +1094,7 @@ describe('default one-shot summarizer', () => { maxTokens: 321, signal: SIGNAL, sessionId: session.id, + compact: true, }) const instruction = adapter.lastOptions?.messages.at(-1)?.content[0] expect(instruction?.type === 'text' ? instruction.text : '').toContain('## Primary Request and Intent') diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index ad7916f918..1a392671d6 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -38,7 +38,7 @@ The plugin registers the single provider route `deepseek`. A request selects it ## App attribution -Every request carries the shared attribution header from dsh-llm's `attributionHeaders()` - the mandatory `User-Agent` baseline identifying the harness (see [dsh-llm § App attribution](../llm/README.md#app-attribution-attributionts)). Direct DeepSeek requests and OpenAI-compatible gateway requests get no provider-specific app-attribution headers under this adapter contract; OpenRouter app attribution is deferred to a future explicit OpenRouter adapter or mode. +Every request carries the shared attribution header from dsh-llm's `attributionHeaders()` - the mandatory `User-Agent` baseline identifying the harness (see [dsh-llm § App attribution](../llm/README.md#app-attribution-attributionts)). Direct DeepSeek requests and OpenAI-compatible gateway requests get no provider-specific app-attribution headers under this adapter contract; OpenRouter app attribution is deferred to a future explicit OpenRouter adapter or mode. A request with `GenerateOptions.compact` set (dsh-compact-basic's auxiliary summarization call) additionally carries `x-deepseek-harness-compact: 1`, so the host can separate compaction traffic from conversation requests. ## Wire-format notes (verified live + against the official docs) diff --git a/packages/llm/llm-deepseek/src/adapter.ts b/packages/llm/llm-deepseek/src/adapter.ts index 237751fc39..976346104e 100644 --- a/packages/llm/llm-deepseek/src/adapter.ts +++ b/packages/llm/llm-deepseek/src/adapter.ts @@ -182,6 +182,9 @@ export class DeepSeekAdapter extends LlmAdapter { ...options.sessionId !== undefined ? { 'x-deepseek-harness-session-id': String(options.sessionId) } : {}, + ...options.compact === true + ? { 'x-deepseek-harness-compact': '1' } + : {}, } // TODO(http): adopt the Cordis HTTP service when shared transport configuration diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts index ffceaacd2d..60c3282c3c 100644 --- a/packages/llm/llm-deepseek/tests/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts @@ -129,6 +129,8 @@ describe('DeepSeekAdapter against a mock server', () => { expect(server.headers[0]).not.toHaveProperty('http-referer') expect(server.headers[0]).not.toHaveProperty('x-openrouter-title') expect(server.headers[0]).not.toHaveProperty('x-openrouter-categories') + // A conversation request carries no compaction marker. + expect(server.headers[0]).not.toHaveProperty('x-deepseek-harness-compact') }) it('streams raw chunks through ctx.llm.stream', async () => { @@ -159,6 +161,19 @@ describe('DeepSeekAdapter against a mock server', () => { expect(server.headers[0]?.['x-deepseek-harness-session-id']).toBe('child-session') }) + it('marks the auxiliary compaction call on the wire', async () => { + const server = await mockServer([{ kind: 'sse', events: textEvents }]) + const ctx = await harness(server.url) + + await assemble(ctx, { + model: 'deepseek-v4-flash', + messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }], + compact: true, + }) + + expect(server.headers[0]?.['x-deepseek-harness-compact']).toBe('1') + }) + it('forwards thinking config onto the wire', async () => { const server = await mockServer([{ kind: 'sse', events: textEvents }]) const ctx = await harness(server.url, { thinking: 'disabled', reasoningEffort: 'high' }) diff --git a/packages/llm/llm/src/types.ts b/packages/llm/llm/src/types.ts index 0cb8fa6935..797db4a10d 100644 --- a/packages/llm/llm/src/types.ts +++ b/packages/llm/llm/src/types.ts @@ -226,4 +226,11 @@ export interface GenerateOptions { * it; replay uses it to keep concurrent parent and child cursors independent. */ sessionId?: Branded<'SessionId'> + /** + * Marks the auxiliary compaction (summarization) call. The DeepSeek adapter + * forwards it as the `x-deepseek-harness-compact: 1` request header so the + * host can separate compaction traffic from conversation requests; it never + * enters the model-visible request body. Loop-built requests leave it unset. + */ + compact?: boolean } From f432f252031499951de1c3a0924c98d22abee829 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Wed, 22 Jul 2026 18:55:21 +0800 Subject: [PATCH 2/3] fix(llm): sync compact option catalogs --- docs/core-data-structures/core.md | 7 +++++++ packages/cordis/tool-cordis/src/api-catalog.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md index 763f6e09a7..2766950c01 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -235,6 +235,13 @@ interface GenerateOptions { * it; replay uses it to keep concurrent parent and child cursors independent. */ sessionId?: Branded<'SessionId'> + /** + * Marks the auxiliary compaction (summarization) call. The DeepSeek adapter + * forwards it as the `x-deepseek-harness-compact: 1` request header so the + * host can separate compaction traffic from conversation requests; it never + * enters the model-visible request body. Loop-built requests leave it unset. + */ + compact?: boolean } ``` diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index b92ebd63e5..302785d2aa 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -1334,7 +1334,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'GenerateOptions', - declaration: 'export interface GenerateOptions {\n provider: string;\n model: string;\n messages: Message[];\n system?: string;\n tools?: ToolSchema[];\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n signal?: AbortSignal;\n sessionId?: Branded<\'SessionId\'>;\n}', + declaration: 'export interface GenerateOptions {\n provider: string;\n model: string;\n messages: Message[];\n system?: string;\n tools?: ToolSchema[];\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n signal?: AbortSignal;\n sessionId?: Branded<\'SessionId\'>;\n compact?: boolean;\n}', }, { name: 'GenericCallView', From 5234a40f9c5a65ad84e5db410acb295c4ef01b7b Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Wed, 22 Jul 2026 18:59:49 +0800 Subject: [PATCH 3/3] refactor(llm): classify auxiliary request purpose --- .../feature/2026-06-18-compaction-capability-seam.md | 3 ++- docs/core-data-structures/core.md | 9 ++++----- packages/compact/compact-basic/README.md | 2 +- packages/compact/compact-basic/src/summarizer.ts | 2 +- .../compact/compact-basic/tests/compact-basic.spec.ts | 2 +- packages/cordis/tool-cordis/src/api-catalog.ts | 2 +- packages/llm/llm-deepseek/README.md | 2 +- packages/llm/llm-deepseek/src/adapter.ts | 2 +- packages/llm/llm-deepseek/tests/adapter.spec.ts | 3 +-- packages/llm/llm/src/types.ts | 9 ++++----- 10 files changed, 17 insertions(+), 19 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md b/.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md index a38cc231da..f609473a9c 100644 --- a/.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md +++ b/.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md @@ -31,7 +31,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, trigger, signal)` takes an explicit `'pressure' | 'context-overflow'` trigger and cancellation. It reads only the latest durable routed request; no header means no work, while any routed provider/model target uses the singleton estimator. `compactRegion(start, end, agent, signal?)` uses `agent.session` as its single session identity and keeps an optional signal for manual callers. The default summarizer resolves its target from explicit config, the latest logged routed target, then agent options, and records the provider/model pair after any `llm/stream` routing. It replays the routed request's prefix and appends the compaction directive as a trailing user message so the provider's warm KV cache is reused — see the [summary prefix-cache Agent Note](../bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md). +`compactIfNeeded(agent, trigger, signal)` takes an explicit `'pressure' | 'context-overflow'` trigger and cancellation. It reads only the latest durable routed request; no header means no work, while any routed provider/model target uses the singleton estimator. `compactRegion(start, end, agent, signal?)` uses `agent.session` as its single session identity and keeps an optional signal for manual callers. The default summarizer resolves its target from explicit config, the latest logged routed target, then agent options, and records the provider/model pair after any `llm/stream` routing. It replays the routed request's prefix and appends the compaction directive as a trailing user message so the provider's warm KV cache is reused — see the [summary prefix-cache Agent Note](../bug-fix/2026-07-21-compaction-summary-prefix-cache-reuse.md). The call sets the provider-neutral `GenerateOptions.purpose` to `compaction`; adapters may map that purpose to model-hidden transport metadata, and the DeepSeek adapter sends `x-deepseek-harness-compact: 1`. ### Automatic pressure runs after successful durable step work @@ -108,6 +108,7 @@ Two failure paths, both documented: - **The full algorithm as concrete interface methods** — rejected because it recouples the contract to one retention strategy. Both core methods are abstract; reusable measurement is a separate LLM-family service and `summarize()` is basic's sole hook. - **Compaction on `agent/request` or provisional `agent/pre-step` inputs** — rejected because neither proves the final durable request and both couple generic lifecycle to compaction-specific envelope data. Post-step replay plus canonical overflow recovery covers both successful and rejected calls. +- **A `compact` boolean or untyped request metadata map** — rejected because multiple auxiliary call kinds would become mutually exclusive flags, while an open bag would discard compiler-checked vocabulary. One typed `purpose` discriminant extends with additional call kinds without adding another `GenerateOptions` field. - **A separate `compact/error` event** — rejected: `compact/end` keeps an `error?` field, mirroring `tool/result`'s self-contained error — one event tells success from failure without correlating a sibling. - **Teaching core turn-repair about `compact/*`** — rejected: the log-only orphan is inert, and a core module patched for every future `xxx/start … xxx/end` plugin pair is exactly the coupling the capability-seam architecture exists to avoid. diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md index 2766950c01..34df633982 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -236,12 +236,11 @@ interface GenerateOptions { */ sessionId?: Branded<'SessionId'> /** - * Marks the auxiliary compaction (summarization) call. The DeepSeek adapter - * forwards it as the `x-deepseek-harness-compact: 1` request header so the - * host can separate compaction traffic from conversation requests; it never - * enters the model-visible request body. Loop-built requests leave it unset. + * Provider-neutral classification for an auxiliary model call. Adapters may + * map the purpose to model-hidden transport metadata. Ordinary conversation + * requests leave it unset. */ - compact?: boolean + purpose?: 'compaction' } ``` diff --git a/packages/compact/compact-basic/README.md b/packages/compact/compact-basic/README.md index a9374b9cfd..7279cc029d 100644 --- a/packages/compact/compact-basic/README.md +++ b/packages/compact/compact-basic/README.md @@ -13,7 +13,7 @@ This backend owns the compaction policy: - **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 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 call replays the conversation's own system prompt, tools, and shadowed-region messages verbatim and appends the compaction instruction as the final user message, so it reuses the provider's warm prefix cache instead of invalidating it. It sets `GenerateOptions.compact`, which adapters may forward as request attribution (the DeepSeek adapter sends `x-deepseek-harness-compact: 1`) without touching the model-visible body. Only returned text enters the checkpoint, excluding reasoning and tool calls that would leak private reasoning or create an orphaned call. +- **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 call replays the conversation's own system prompt, tools, and shadowed-region messages verbatim and appends the compaction instruction as the final user message, so it reuses the provider's warm prefix cache instead of invalidating it. It sets `GenerateOptions.purpose` to `compaction`, which adapters may forward as request attribution (the DeepSeek adapter sends `x-deepseek-harness-compact: 1`) without touching the model-visible body. 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()` mutates `agent.session` and records its start, summary, replacement, and end. After asynchronous summarization it rejects a changed surface-node snapshot, while unrelated log-only events may append without invalidating the selected span. The serial `agent/post-step` listener checks pressure after successful output and tool work are durable but before `step/end`. Canonical provider overflow is handled through `agent/request-error` after the failed step closes. - **Overflow recovery** — provider-confirmed overflow needs no capacity metadata: it bypasses normal pressure and retention, prunes, then attempts one maximal balanced head reduction while leaving the newest indivisible unit. Retry is authorized whenever `surface.replaceGeneration` advances, including when pruning lands before later summary work throws. No replacement, an exhausted target-specific cap, cancellation, or an unknown/noncanonical error preserves the original provider failure. diff --git a/packages/compact/compact-basic/src/summarizer.ts b/packages/compact/compact-basic/src/summarizer.ts index 51adc19c60..ce4f28f8b3 100644 --- a/packages/compact/compact-basic/src/summarizer.ts +++ b/packages/compact/compact-basic/src/summarizer.ts @@ -138,7 +138,7 @@ export async function summarizeWithLlm( ...input.tools === undefined ? {} : { tools: [...input.tools] }, maxTokens: config.maxTokens, sessionId: agent.session.id, - compact: true, + purpose: 'compaction', ...signal === undefined ? {} : { signal }, } for await (const chunk of ctx.llm.stream(options)) assembler.push(chunk) diff --git a/packages/compact/compact-basic/tests/compact-basic.spec.ts b/packages/compact/compact-basic/tests/compact-basic.spec.ts index 97d0127465..3ea658b64d 100644 --- a/packages/compact/compact-basic/tests/compact-basic.spec.ts +++ b/packages/compact/compact-basic/tests/compact-basic.spec.ts @@ -1094,7 +1094,7 @@ describe('default one-shot summarizer', () => { maxTokens: 321, signal: SIGNAL, sessionId: session.id, - compact: true, + purpose: 'compaction', }) const instruction = adapter.lastOptions?.messages.at(-1)?.content[0] expect(instruction?.type === 'text' ? instruction.text : '').toContain('## Primary Request and Intent') diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 302785d2aa..1929fa7c81 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -1334,7 +1334,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'GenerateOptions', - declaration: 'export interface GenerateOptions {\n provider: string;\n model: string;\n messages: Message[];\n system?: string;\n tools?: ToolSchema[];\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n signal?: AbortSignal;\n sessionId?: Branded<\'SessionId\'>;\n compact?: boolean;\n}', + declaration: 'export interface GenerateOptions {\n provider: string;\n model: string;\n messages: Message[];\n system?: string;\n tools?: ToolSchema[];\n temperature?: number;\n maxTokens?: number;\n stop?: string[];\n signal?: AbortSignal;\n sessionId?: Branded<\'SessionId\'>;\n purpose?: \'compaction\';\n}', }, { name: 'GenericCallView', diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index 1a392671d6..162843beb3 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -38,7 +38,7 @@ The plugin registers the single provider route `deepseek`. A request selects it ## App attribution -Every request carries the shared attribution header from dsh-llm's `attributionHeaders()` - the mandatory `User-Agent` baseline identifying the harness (see [dsh-llm § App attribution](../llm/README.md#app-attribution-attributionts)). Direct DeepSeek requests and OpenAI-compatible gateway requests get no provider-specific app-attribution headers under this adapter contract; OpenRouter app attribution is deferred to a future explicit OpenRouter adapter or mode. A request with `GenerateOptions.compact` set (dsh-compact-basic's auxiliary summarization call) additionally carries `x-deepseek-harness-compact: 1`, so the host can separate compaction traffic from conversation requests. +Every request carries the shared attribution header from dsh-llm's `attributionHeaders()` - the mandatory `User-Agent` baseline identifying the harness (see [dsh-llm § App attribution](../llm/README.md#app-attribution-attributionts)). Direct DeepSeek requests and OpenAI-compatible gateway requests get no provider-specific app-attribution headers under this adapter contract; OpenRouter app attribution is deferred to a future explicit OpenRouter adapter or mode. A request whose `GenerateOptions.purpose` is `compaction` (dsh-compact-basic's auxiliary summarization call) additionally carries `x-deepseek-harness-compact: 1`, so the host can separate compaction traffic from conversation requests. ## Wire-format notes (verified live + against the official docs) diff --git a/packages/llm/llm-deepseek/src/adapter.ts b/packages/llm/llm-deepseek/src/adapter.ts index 976346104e..3ca81f678c 100644 --- a/packages/llm/llm-deepseek/src/adapter.ts +++ b/packages/llm/llm-deepseek/src/adapter.ts @@ -182,7 +182,7 @@ export class DeepSeekAdapter extends LlmAdapter { ...options.sessionId !== undefined ? { 'x-deepseek-harness-session-id': String(options.sessionId) } : {}, - ...options.compact === true + ...options.purpose === 'compaction' ? { 'x-deepseek-harness-compact': '1' } : {}, } diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts index 60c3282c3c..f147323645 100644 --- a/packages/llm/llm-deepseek/tests/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts @@ -129,7 +129,6 @@ describe('DeepSeekAdapter against a mock server', () => { expect(server.headers[0]).not.toHaveProperty('http-referer') expect(server.headers[0]).not.toHaveProperty('x-openrouter-title') expect(server.headers[0]).not.toHaveProperty('x-openrouter-categories') - // A conversation request carries no compaction marker. expect(server.headers[0]).not.toHaveProperty('x-deepseek-harness-compact') }) @@ -168,7 +167,7 @@ describe('DeepSeekAdapter against a mock server', () => { await assemble(ctx, { model: 'deepseek-v4-flash', messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }], - compact: true, + purpose: 'compaction', }) expect(server.headers[0]?.['x-deepseek-harness-compact']).toBe('1') diff --git a/packages/llm/llm/src/types.ts b/packages/llm/llm/src/types.ts index 797db4a10d..12febecf42 100644 --- a/packages/llm/llm/src/types.ts +++ b/packages/llm/llm/src/types.ts @@ -227,10 +227,9 @@ export interface GenerateOptions { */ sessionId?: Branded<'SessionId'> /** - * Marks the auxiliary compaction (summarization) call. The DeepSeek adapter - * forwards it as the `x-deepseek-harness-compact: 1` request header so the - * host can separate compaction traffic from conversation requests; it never - * enters the model-visible request body. Loop-built requests leave it unset. + * Provider-neutral classification for an auxiliary model call. Adapters may + * map the purpose to model-hidden transport metadata. Ordinary conversation + * requests leave it unset. */ - compact?: boolean + purpose?: 'compaction' }