mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts: # .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml # docs/config-catalog.md # docs/cordis-catalog/events.md # docs/cordis-catalog/services.md # docs/core-data-structures/core.md # docs/core-data-structures/core.zh.md # docs/core-data-structures/llm-streaming.i18n.yaml # docs/core-data-structures/llm-streaming.md # docs/core-data-structures/llm-streaming.zh.md # docs/event-producer-consumer.md # docs/module-graph.md # docs/subsystems/attachment.i18n.yaml # docs/subsystems/attachment.md # docs/subsystems/attachment.zh.md # docs/subsystems/core.i18n.yaml # examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl # packages/README.i18n.yaml # packages/README.md # packages/README.zh.md # packages/client/runtime/package.json # packages/client/ui-conversation/package.json # packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx # packages/client/ui-conversation/src/client/chat/ChatView.tsx # packages/client/ui-conversation/src/client/chat/MessageItem.tsx # packages/client/ui-conversation/src/client/index.ts # packages/client/ui-conversation/tests/input-bar.spec.tsx # packages/compact/compact-basic/README.i18n.yaml # packages/compact/compact-basic/README.md # packages/compact/compact-basic/README.zh.md # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/index.ts # packages/host/apiproxy/src/api/sessions.ts # packages/host/apiproxy/src/index.ts # packages/host/apiproxy/tests/api-proxy-models.spec.ts # packages/self-modification/tool-cordis/src/api-catalog.ts # pnpm-lock.yaml # scripts/type-equiv.manifest.json
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/compact/compact-basic/README.md
|
||||
README.md: 9bea77045c60755ed4280d3239472df456903cc8
|
||||
README.zh.md: eef454c10b40081655c287815b65f34ae0c5ae47
|
||||
README.md: 97c71cafe1eba7dd29c903535b378439586e807e
|
||||
README.zh.md: 55a0080fe3c08e5559d737d46f57e23645a52cad
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
The **basic compaction backend**: a `BasicCompactService` implementing the `@deepseek-ai/dsh-compact` seam with reusable `ctx.tokenMeter` pressure, token-budget retention, and summarization as a direct one-shot `ctx.llm.stream()` call that replays the conversation prefix to reuse the provider's KV cache (interceptable at `llm/stream`).
|
||||
The **basic compaction backend**: a `BasicCompactService` implementing the `@deepseek-ai/dsh-compact` Service Definition with reusable `ctx.tokenMeter` pressure, token-budget retention, and summarization as a direct one-shot `ctx.llm.stream()` call that replays the conversation prefix to reuse the provider's KV cache (interceptable at `llm/stream`).
|
||||
|
||||
This is the implementation tier of the compaction capability — see the [interface package](../compact/README.md) for the seam and the [capability-seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md) for the design.
|
||||
This package owns the Service provider role of the compaction capability — see the [Service Definition package](../compact/README.md) for its contract and the [capability-seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md) for the design.
|
||||
|
||||
## What it owns
|
||||
|
||||
@@ -15,13 +15,13 @@ 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 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, including image references, and appends the compaction instruction as the final user message, so it reuses the provider's warm prefix cache instead of invalidating it. The selected adapter must resolve or explicitly reject those images. 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; image output fails with `UNSUPPORTED_CONTENT` rather than disappearing.
|
||||
- **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.
|
||||
- **Lifecycle** — all entry points share one bracket-first region transaction. It validates the range and live lock, appends `compact/start` synchronously, prepares and awaits the summary, revalidates, appends provenance plus the replacement, and makes exactly one closing attempt. Automatic and explicit-region calls require a numeric open-turn owner and whole-surface stability; the serial `agent/pre-step` listener checks pressure before request derivation, while canonical provider overflow enters through `agent/request-error` and authorizes retry only after durable surface progress. `compactNow()` reserves idle admission, uses `turn: null`, accepts append-only context outside its selected span, flushes every closed attempt, and releases admission in `finally`.
|
||||
- **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` extension point. The call replays the conversation's own system prompt, tools, and shadowed-region messages verbatim, including image references, and appends the compaction instruction as the final user message, so it reuses the provider's warm prefix cache instead of invalidating it. The selected adapter must resolve or explicitly reject those images. 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; image output fails with `UNSUPPORTED_CONTENT` rather than disappearing.
|
||||
- **Framing** — the replacement user message marks established checkpoint context with `<compacted-summary>` tags. The raw summary remains on the `compact/summary` event, and later automatic cycles merge the prior checkpoint.
|
||||
- **Lifecycle** — all entry points share one bracket-first region transaction. It validates the range and live lock, appends `compact/start` synchronously, prepares and awaits the summary, revalidates, appends `compact/summary` plus the replacement, and makes exactly one closing attempt. Automatic and explicit-region calls require a numeric open-turn owner and whole-surface stability; the serial `agent/pre-step` listener checks pressure before request derivation, while canonical provider overflow enters through `agent/request-error` and authorizes retry only after durable surface progress. `compactNow()` reserves idle admission, uses `turn: null`, accepts append-only context outside its selected span, flushes every closed attempt, and releases admission in `finally`.
|
||||
- **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.
|
||||
- **Failure handling** — a live unmatched `compact/start` is the durable lock. An unmatched marker before a newer `session/end-seed` is stale evidence from a prior lifecycle and does not block; one after that boundary reports `busy`. Summary and changed-span failures close with an error and leave the conversation surface untouched, though the attempt remains in the log. A failed close deliberately leaves a blocking orphan. Operational pressure failures warn and continue, while overflow-recovery failure preserves the original provider error only when no earlier replacement advanced the surface. Cancellation remains authoritative after cleanup and durability.
|
||||
|
||||
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 safe summary plus the complete provider output, call envelope, and usage when available (`{ summary, rawOutput?, llmStreamCall?, provider, model, maxTokens?, usage? }`); `llmStreamCall: true` means producing that result consumed exactly one call through this context's `ctx.llm.stream()` and requires complete `rawOutput`, while unmarked `rawOutput` does not identify the call path. The transaction preserves those fields on `compact/summary`.
|
||||
The protected `summarize()` method is the sole subclass hook. A template- or remote-summarizer subclass can override it while pressure, retention, cited source events, shrink validation, and shadowed-token accounting stay on `ctx.tokenMeter`. The hook returns the safe summary plus the complete provider output, call envelope, and usage when available (`{ summary, rawOutput?, llmStreamCall?, provider, model, maxTokens?, usage? }`); `llmStreamCall: true` means producing that result consumed exactly one call through this context's `ctx.llm.stream()` and requires complete `rawOutput`, while unmarked `rawOutput` does not identify the call path. The transaction preserves those fields on `compact/summary`.
|
||||
|
||||
## Config (`BasicCompactConfig`)
|
||||
|
||||
@@ -106,7 +106,7 @@ Replacing rather than append-only. Each checkpoint invalidates reuse from the fi
|
||||
|
||||
#### What the model sees
|
||||
|
||||
The summarization model receives the conversation replayed verbatim — the same system prompt, tool schemas, and messages, including image references, that the last routed request sent for the shadowed region — followed by one final user message: the compaction instruction below. The conversation model never sees this private request or its reasoning; only returned text is stored.
|
||||
The summarization model receives the conversation replayed verbatim — the same system prompt, tool schemas, and messages the last routed request sent for the shadowed region — followed by one final user message: the compaction instruction below. The conversation model never sees this private request or its reasoning; only returned text is stored.
|
||||
|
||||
##### Compaction instruction (final user message)
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
**基础压缩(compaction)后端**:`BasicCompactService` 实现 `@deepseek-ai/dsh-compact` seam,使用可复用的 `ctx.tokenMeter` 压力、token 预算保留与摘要。摘要是直接的一次性 `ctx.llm.stream()` 调用,它会回放会话前缀以复用提供方的 KV Cache(可在 `llm/stream` 处拦截)。
|
||||
**基础压缩(compaction)后端**:`BasicCompactService` 实现 `@deepseek-ai/dsh-compact` Service Definition,使用可复用的 `ctx.tokenMeter` 压力、token 预算保留与摘要。摘要是直接的一次性 `ctx.llm.stream()` 调用,它会回放会话前缀以复用提供方的 KV Cache(可在 `llm/stream` 处拦截)。
|
||||
|
||||
这是压缩能力的实现层。seam 见 [接口包](../compact/README.md),设计见 [能力 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md)。
|
||||
本包承担压缩能力的 Service provider 角色;其约定见 [Service Definition 包](../compact/README.md),设计见 [能力 seam Agent Note](../../../.agents/notes/implemented/feature/2026-06-18-compaction-capability-seam.md)。
|
||||
|
||||
## 拥有的职责
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
- **不依赖模型的剪枝**:在压力或规范溢出符合条件后,可选的 [`ctx.toolResultPrune`](../compact-tool-result-prune/README.md) 服务会在选择范围之前改写超大工具结果。Compact-basic 通过 `ctx.tokenMeter` 重新测量;如果压力已回到安全范围,就跳过摘要,否则对已剪枝的表层进行摘要。低于压力的步骤检查绝不剪枝。
|
||||
- **保留**:压缩最旧的完整表层单元,同时保留近期尾部,并通过 [`dsh-compact` 边界 helper](../compact/README.md#tool-pairing-boundaries) 将切分点调整到工具调用/结果配对平衡的位置。轮次边界不会保护失控轮次内的旧步骤。尚未闭合且不可分的尾部会在闭合前拒绝压缩。当闭合的超大工具单元以文本型结果为可移除主体时,可选 pruner 可以修复它;不可分的非工具单元与不可剪枝的工具剩余部分不在范围内。
|
||||
- **收敛**:最多按 `compactionRetries` 重试头部检查点压缩;拒绝不能缩小源内容的摘要,如果重试仍无法回到阈值以下,则抛出异常。
|
||||
- **摘要**:直接 `llm/stream` 调用使用已配置的提供方/模型对与上限,回退到最新已记录请求目标,然后再回退到 agent 目标,而不运行仅用于 agent loop 的 `agent/request` seam。该调用会逐字回放会话自身的系统提示词、工具与已遮蔽区域消息(包括图片引用),并将压缩指令作为最后一条 user 消息追加,从而复用提供方的热前缀 cache,而非使它失效。所选适配器必须解析或明确拒绝这些图片。它将 `GenerateOptions.purpose` 设为 `compaction`,适配器可将其作为请求归因转发(DeepSeek 适配器发送 `x-deepseek-harness-compact: 1`),但不会触碰模型可见的请求体。只有返回的文本会进入检查点;推理(reasoning)和工具调用都会被排除,以免泄露私有推理或产生遗留调用;图片输出会以 `UNSUPPORTED_CONTENT` 失败,而不是消失。
|
||||
- **框定**:替换 user 消息使用 `<compacted-summary>` 标签标记已建立的检查点上下文。原始摘要保留在溯源事件上,后续自动周期会合并之前的检查点。
|
||||
- **生命周期**:所有入口点共享一个先记录标记的区域事务。它会验证范围与活动锁,同步追加 `compact/start`,准备并等待摘要,重新验证,再追加溯源信息和替换,最后恰好进行一次闭合尝试。自动调用和显式范围调用要求数字标识的开放轮次归属,并要求整个表层保持稳定;串行 `agent/pre-step` listener 会在派生请求之前检查压力,而规范提供方溢出则经由 `agent/request-error` 进入,并且只在表层取得持久进展后才允许重试。`compactNow()` 会预留空闲接纳,使用 `turn: null`,允许所选 span 之外追加仅追加上下文,flush 每次已闭合尝试,并在 `finally` 中释放接纳预留。
|
||||
- **摘要**:直接 `llm/stream` 调用使用已配置的提供方/模型对与上限,回退到最新已记录请求目标,然后再回退到 agent 目标,而不运行仅用于 agent loop 的 `agent/request` 扩展点。该调用会逐字回放会话自身的系统提示词、工具与已遮蔽区域消息(包括图片引用),并将压缩指令作为最后一条 user 消息追加,从而复用提供方的热前缀 cache,而非使它失效。所选适配器必须解析或明确拒绝这些图片。它将 `GenerateOptions.purpose` 设为 `compaction`,适配器可将其作为请求归因转发(DeepSeek 适配器发送 `x-deepseek-harness-compact: 1`),但不会触碰模型可见的请求体。只有返回的文本会进入检查点;推理(reasoning)和工具调用都会被排除,以免泄露私有推理或产生遗留调用;图片输出会以 `UNSUPPORTED_CONTENT` 失败,而不是消失。
|
||||
- **框定**:替换 user 消息使用 `<compacted-summary>` 标签标记已建立的检查点上下文。原始摘要保留在 `compact/summary` 事件上,后续自动周期会合并之前的检查点。
|
||||
- **生命周期**:所有入口点共享一个先记录标记的区域事务。它会验证范围与活动锁,同步追加 `compact/start`,准备并等待摘要,重新验证,再追加 `compact/summary` 和替换,最后恰好进行一次闭合尝试。自动调用和显式范围调用要求数字标识的开放轮次归属,并要求整个表层保持稳定;串行 `agent/pre-step` listener 会在派生请求之前检查压力,而规范提供方溢出则经由 `agent/request-error` 进入,并且只在表层取得持久进展后才允许重试。`compactNow()` 会预留空闲接纳,使用 `turn: null`,允许所选 span 之外追加仅追加上下文,flush 每次已闭合尝试,并在 `finally` 中释放接纳预留。
|
||||
- **溢出恢复**:提供方已确认的溢出不需容量元数据。它会绕过常规压力与保留,执行剪枝,再尝试一次最大平衡头部缩减,并留下最新不可分单元。只要 `surface.replaceGeneration` 前进,就允许重试,包括剪枝在后续摘要工作抛出异常前已落地的情况。如果没有替换、目标特定上限已耗尽、已取消,或遇到未知/非规范错误,则保留原始提供方失败。
|
||||
- **失败处理**:活动的未匹配 `compact/start` 是持久锁。位于较新 `session/end-seed` 之前的未匹配标记,是先前生命周期留下的陈旧证据,不会阻塞;位于该边界之后的标记报告 `busy`。摘要和 span 变更失败会以错误闭合,并保持会话表层不变,但日志中仍保留该尝试。闭合失败会有意留下阻塞性的未匹配标记。压力检查中的运行故障会发出警告并继续;只有此前没有替换推进表层时,溢出恢复失败才保留原始提供方错误。完成清理与持久化后,取消仍具有最终决定权。
|
||||
|
||||
受保护的 `summarize()` 方法是唯一的子类钩子。基于模板或远程摘要器的子类可以覆盖该方法,同时压力、保留、溯源、缩减验证与已遮蔽 token 计量仍由 `ctx.tokenMeter` 负责。钩子返回安全摘要,以及完整提供方输出、调用 envelope 和可用时的 usage(`{ summary, rawOutput?, llmStreamCall?, provider, model, maxTokens?, usage? }`);`llmStreamCall: true` 表示生成该结果时恰好通过此上下文的 `ctx.llm.stream()` 发起了一次调用,且必须提供完整的 `rawOutput`;未带标记的 `rawOutput` 并不能判定调用路径。事务会在 `compact/summary` 上保留这些字段。
|
||||
受保护的 `summarize()` 方法是唯一的子类钩子。基于模板或远程摘要器的子类可以覆盖该方法,同时压力、保留、被引用的源事件、缩减验证与已遮蔽 token 计量仍由 `ctx.tokenMeter` 负责。钩子返回安全摘要,以及完整提供方输出、调用 envelope 和可用时的 usage(`{ summary, rawOutput?, llmStreamCall?, provider, model, maxTokens?, usage? }`);`llmStreamCall: true` 表示生成该结果时恰好通过此上下文的 `ctx.llm.stream()` 发起了一次调用,且必须提供完整的 `rawOutput`;未带标记的 `rawOutput` 并不能判定调用路径。事务会在 `compact/summary` 上保留这些字段。
|
||||
|
||||
## 配置(`BasicCompactConfig`)
|
||||
|
||||
@@ -106,7 +106,7 @@ This is an automatically generated checkpoint condensing an earlier span of the
|
||||
|
||||
#### 模型看到的内容
|
||||
|
||||
摘要模型会接收逐字回放的会话:与上次已路由请求为已遮蔽区域发送的相同系统提示词、工具 schema 与消息(包括图片引用),后面跟随一条最终 user 消息,即下方压缩指令。会话模型绝不会看到该私有请求或其推理;只有返回文本会被存储。
|
||||
摘要模型会接收逐字回放的会话:与上次已路由请求为已遮蔽区域发送的相同系统提示词、工具 schema 与消息,后面跟随一条最终 user 消息,即下方压缩指令。会话模型绝不会看到该私有请求或其推理;只有返回文本会被存储。
|
||||
|
||||
##### 压缩指令(最终 user 消息)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-agent": "^0.0.1",
|
||||
"@deepseek-ai/dsh-compact": "^0.0.1",
|
||||
"@deepseek-ai/dsh-commands": "^0.0.1",
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"@deepseek-ai/dsh-llm": "^0.0.1",
|
||||
"@deepseek-ai/dsh-session": "^0.0.1",
|
||||
@@ -49,6 +50,7 @@
|
||||
"@deepseek-ai/dsh-agent-loop": "workspace:^",
|
||||
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:^",
|
||||
"@deepseek-ai/dsh-compact": "workspace:^",
|
||||
"@deepseek-ai/dsh-commands": "workspace:^",
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm": "workspace:^",
|
||||
"@deepseek-ai/dsh-llm-retry": "workspace:^",
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { Session } from '@deepseek-ai/dsh-session'
|
||||
import { CONTEXT_WINDOW_EXCEEDED_CODE, assertNever } from '@deepseek-ai/dsh-llm'
|
||||
import type { LlmCallConfig } from '@deepseek-ai/dsh-llm'
|
||||
import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
|
||||
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||
// Type-only: makes the optional sibling service available to `ctx.get()`.
|
||||
import type {} from '@deepseek-ai/dsh-compact-tool-result-prune'
|
||||
import {
|
||||
@@ -93,7 +94,7 @@ const modelPolicy: z<ModelCompactPolicyConfig> = z.object({
|
||||
|
||||
/**
|
||||
* Dependency-light compaction backend using `ctx.tokenMeter` for pressure,
|
||||
* retention, provenance, and summary-convergence pricing.
|
||||
* retention, cited source events, and summary-convergence pricing.
|
||||
*
|
||||
* `summarize()` is the sole subclass customization hook; the replay and durable
|
||||
* mutation strategy stays fixed so every pricing decision uses the singleton
|
||||
@@ -230,7 +231,7 @@ export class BasicCompactService extends CompactService {
|
||||
* @param input - replayed conversation prefix (system, tools, and leading messages) to condense.
|
||||
* @param agent - supplies routed-model history, fallback model, and session id.
|
||||
* @param signal - optional cancellation forwarded to the adapter.
|
||||
* @returns safe text summary blocks and exact auxiliary-call provenance.
|
||||
* @returns safe text summary blocks and the exact auxiliary call envelope and output.
|
||||
*/
|
||||
protected async summarize(
|
||||
input: SummarizationInput,
|
||||
@@ -361,9 +362,14 @@ export class BasicCompactService extends CompactService {
|
||||
* resolve only after its standalone marker pair is durably checkpointed.
|
||||
* @param agent - idle agent whose next-turn admission this call reserves.
|
||||
* @param signal - cancellation scoped to this compaction request.
|
||||
* @param sourceCommandId - initiating command identity for presentation correlation.
|
||||
* @returns the committed result, or `null` when no safe useful range exists.
|
||||
*/
|
||||
override compactNow(agent: Agent, signal: AbortSignal): Promise<CompactionResult | null> {
|
||||
override compactNow(
|
||||
agent: Agent,
|
||||
signal: AbortSignal,
|
||||
sourceCommandId?: CommandId,
|
||||
): Promise<CompactionResult | null> {
|
||||
signal.throwIfAborted()
|
||||
try {
|
||||
return agent.runMaintenance(async (agentSignal) => {
|
||||
@@ -385,6 +391,7 @@ export class BasicCompactService extends CompactService {
|
||||
{
|
||||
owner: null,
|
||||
stability: 'selected-span',
|
||||
...sourceCommandId === undefined ? {} : { sourceCommandId },
|
||||
flush: async () => {
|
||||
await this.ctx.sessions.flush(agent.session)
|
||||
},
|
||||
|
||||
@@ -5,14 +5,17 @@
|
||||
* @module @deepseek-ai/dsh-compact-basic/region
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { isDeepStrictEqual } from 'node:util'
|
||||
import {
|
||||
COMPACT_CHECKPOINT_SOURCE,
|
||||
CompactionId,
|
||||
ManualCompactionError,
|
||||
compactCheckpointSource,
|
||||
toolPairingBalancedAfter,
|
||||
toolPairingBalancedBefore,
|
||||
} from '@deepseek-ai/dsh-compact'
|
||||
import type { CompactionResult } from '@deepseek-ai/dsh-compact'
|
||||
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||
import { createUserMessage, errorChain } from '@deepseek-ai/dsh-llm'
|
||||
import type { Message, UserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { TokenMeasurement, TokenMeterService } from '@deepseek-ai/dsh-token-meter'
|
||||
@@ -54,6 +57,8 @@ interface CompactionTransactionOptions {
|
||||
readonly stability: 'whole-surface' | 'selected-span'
|
||||
/** Optional durability checkpoint after a successfully closed bracket. */
|
||||
readonly flush?: () => Promise<void>
|
||||
/** Manual command that initiated this transaction, when present. */
|
||||
readonly sourceCommandId?: CommandId
|
||||
}
|
||||
|
||||
interface CompactionEntryState {
|
||||
@@ -175,7 +180,13 @@ export async function compactSurfaceRegion(
|
||||
owner = entryState.openTurn
|
||||
}
|
||||
|
||||
const startEvent = session.append('compact/start', { turn: owner })
|
||||
const compactionId = CompactionId(randomUUID())
|
||||
const lifecycle = {
|
||||
compactionId,
|
||||
...options.sourceCommandId === undefined ? {} : { sourceCommandId: options.sourceCommandId },
|
||||
turn: owner,
|
||||
}
|
||||
const startEvent = session.append('compact/start', lifecycle)
|
||||
const assertStable: StabilityCheck = options.stability === 'whole-surface'
|
||||
? assertWholeSurfaceUnchanged
|
||||
: assertSelectedSpanStable
|
||||
@@ -188,13 +199,20 @@ export async function compactSurfaceRegion(
|
||||
|
||||
try {
|
||||
const prepared = prepareCompaction(dependencies, session, selection)
|
||||
const summarized = await summarizeCompaction(dependencies, prepared, agent, signal)
|
||||
const summarized = await summarizeCompaction(
|
||||
dependencies,
|
||||
prepared,
|
||||
agent,
|
||||
compactionId,
|
||||
options.sourceCommandId,
|
||||
signal,
|
||||
)
|
||||
if (options.owner === null) signal?.throwIfAborted()
|
||||
assertStable(dependencies, session, summarized)
|
||||
stage = 'commit'
|
||||
const pending = commitCompactionBody(session, startEvent, summarized)
|
||||
closing = true
|
||||
const endEvent = session.append('compact/end', { turn: owner })
|
||||
const endEvent = session.append('compact/end', lifecycle)
|
||||
closed = true
|
||||
result = completeCompaction(pending, endEvent)
|
||||
} catch (error: unknown) {
|
||||
@@ -202,7 +220,7 @@ export async function compactSurfaceRegion(
|
||||
if (!closing) {
|
||||
closing = true
|
||||
try {
|
||||
session.append('compact/end', { turn: owner, error: errorChain(error) })
|
||||
session.append('compact/end', { ...lifecycle, error: errorChain(error) })
|
||||
closed = true
|
||||
} catch (closeError: unknown) {
|
||||
failure = { error: closeError, stage: 'commit' }
|
||||
@@ -343,12 +361,14 @@ async function summarizeCompaction(
|
||||
dependencies: RegionDependencies,
|
||||
prepared: PreparedCompaction,
|
||||
agent: Agent,
|
||||
compactionId: CompactionResult['compactionId'],
|
||||
sourceCommandId: CommandId | undefined,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SummarizedCompaction> {
|
||||
const summaryResult = await dependencies.summarize(prepared.input, agent, signal)
|
||||
const checkpointMessage = createUserMessage({
|
||||
content: frameSummary(summaryResult.summary),
|
||||
source: COMPACT_CHECKPOINT_SOURCE,
|
||||
source: compactCheckpointSource(compactionId, sourceCommandId),
|
||||
})
|
||||
const framedSummaryTokenCount = dependencies.meter.estimateMessage(checkpointMessage)
|
||||
if (framedSummaryTokenCount >= prepared.shadowedTokenCount) {
|
||||
@@ -403,7 +423,7 @@ function assertSelectedSpanStable(
|
||||
}
|
||||
}
|
||||
|
||||
/** Append one already-summarized provenance and replacement body without yielding. */
|
||||
/** Append one completed summary record and replacement body without yielding. */
|
||||
function commitCompactionBody(
|
||||
session: Session,
|
||||
startEvent: SessionEvent<'compact/start'>,
|
||||
@@ -425,6 +445,10 @@ function commitCompactionBody(
|
||||
? { rawOutput: summarized.rawOutput, llmStreamCall: true as const }
|
||||
: summarized.rawOutput === undefined ? {} : { rawOutput: summarized.rawOutput }
|
||||
const summaryEvent = session.append('compact/summary', {
|
||||
compactionId: startEvent.data.compactionId,
|
||||
...startEvent.data.sourceCommandId === undefined
|
||||
? {}
|
||||
: { sourceCommandId: startEvent.data.sourceCommandId },
|
||||
summary,
|
||||
...callProvenance,
|
||||
shadowedRange: { start, end },
|
||||
@@ -440,6 +464,10 @@ function commitCompactionBody(
|
||||
sourceEventSeqs: [startEvent.seq, summaryEvent.seq, ...shadowedSeqs],
|
||||
})
|
||||
return {
|
||||
compactionId: startEvent.data.compactionId,
|
||||
...startEvent.data.sourceCommandId === undefined
|
||||
? {}
|
||||
: { sourceCommandId: startEvent.data.sourceCommandId },
|
||||
startSeq: startEvent.seq,
|
||||
summarySeq: summaryEvent.seq,
|
||||
summary,
|
||||
|
||||
@@ -84,7 +84,7 @@ export interface SummarizationInput {
|
||||
readonly messages: readonly Message[]
|
||||
}
|
||||
|
||||
/** Safe summary content plus the exact auxiliary call envelope recorded in provenance. */
|
||||
/** Safe summary content plus the exact auxiliary call envelope recorded with it. */
|
||||
export type SummaryResult = {
|
||||
summary: ContentBlock[]
|
||||
provider: string
|
||||
@@ -116,7 +116,7 @@ export type SummaryResult = {
|
||||
* @param input - replayed conversation prefix (system, tools, and leading messages) to condense.
|
||||
* @param agent - supplies routed-model history, fallback model, and session id.
|
||||
* @param signal - optional cancellation forwarded to the adapter.
|
||||
* @returns safe text-only summary blocks and exact call provenance.
|
||||
* @returns safe text-only summary blocks and the exact call envelope and output.
|
||||
*/
|
||||
export async function summarizeWithLlm(
|
||||
ctx: Context,
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 type { SummarizationInput, SummaryResult } from '@deepseek-ai/dsh-compact-basic/src/summarizer.ts'
|
||||
import { toolPairingBalancedAfter, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compact'
|
||||
import { CompactionId, toolPairingBalancedAfter, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compact'
|
||||
import {
|
||||
resolveCompactSpec,
|
||||
resolveConfig,
|
||||
@@ -839,7 +839,7 @@ describe('optional model-free tool-result pruning', () => {
|
||||
})
|
||||
|
||||
describe('compaction region transaction', () => {
|
||||
it('lands a framed, replayable checkpoint with exact pricing provenance', async () => {
|
||||
it('lands a framed, replayable checkpoint with exact source seqs and token price', async () => {
|
||||
const compact = service()
|
||||
compact.rawOutput = [
|
||||
{ type: 'reasoning', text: 'private compact thought' },
|
||||
@@ -946,7 +946,10 @@ describe('compaction region transaction', () => {
|
||||
)).rejects.toThrow(/no open turn/)
|
||||
|
||||
const locked = conversation(1)
|
||||
locked.append('compact/start', { turn: 2 })
|
||||
locked.append('compact/start', {
|
||||
compactionId: CompactionId('locked-compaction'),
|
||||
turn: 2,
|
||||
})
|
||||
const lockedNodes = locked.surface.nodes
|
||||
await expect(compact.compactRegion(
|
||||
lockedNodes[0]!,
|
||||
@@ -1673,6 +1676,7 @@ describe('automatic listener and loader composition', () => {
|
||||
const compact = new TestCompactService(ctx)
|
||||
const session = conversation(2)
|
||||
const fakeResult: CompactionResult = {
|
||||
compactionId: CompactionId('fake-compaction'),
|
||||
startSeq: 1,
|
||||
summarySeq: 2,
|
||||
endSeq: 3,
|
||||
|
||||
@@ -3,13 +3,14 @@ import { Context } from 'cordis'
|
||||
import AgentLoop from '@deepseek-ai/dsh-agent-loop'
|
||||
import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
|
||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
import { CommandId } from '@deepseek-ai/dsh-commands/brand'
|
||||
import * as SessionInvariant from '@deepseek-ai/dsh-session/invariant'
|
||||
import * as AgentInvariant from '@deepseek-ai/dsh-agent/invariant'
|
||||
import * as AgentLoopInvariant from '@deepseek-ai/dsh-agent-loop/invariant'
|
||||
import * as CompactInvariant from '@deepseek-ai/dsh-compact/invariant'
|
||||
import * as CompactBasicInvariant from '@deepseek-ai/dsh-compact-basic/invariant'
|
||||
import { BasicCompactService } from '@deepseek-ai/dsh-compact-basic'
|
||||
import { isCompactCheckpointSource, ManualCompactionError } from '@deepseek-ai/dsh-compact'
|
||||
import { CompactionId, isCompactCheckpointSource, ManualCompactionError } from '@deepseek-ai/dsh-compact'
|
||||
import type { CompactionResult } from '@deepseek-ai/dsh-compact'
|
||||
import {
|
||||
createAssistantMessage,
|
||||
@@ -23,7 +24,7 @@ import type {
|
||||
StreamChunk,
|
||||
TokenUsage,
|
||||
} from '@deepseek-ai/dsh-llm'
|
||||
import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session'
|
||||
import SessionStore, { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
|
||||
import LlmService from '@deepseek-ai/dsh-llm'
|
||||
import TokenMeterService from '@deepseek-ai/dsh-token-meter'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
@@ -394,22 +395,35 @@ describe('compactNow transaction and failure classification', () => {
|
||||
const { compact, flushes } = detachedService()
|
||||
const session = closedConversation(2, 7)
|
||||
const agent = fakeAgent(session, () => () => undefined)
|
||||
const commandId = CommandId('manual-compact-command')
|
||||
|
||||
const result = await compact.compactNow(agent, SIGNAL)
|
||||
const result = await compact.compactNow(agent, SIGNAL, commandId)
|
||||
|
||||
expect(result).not.toBeNull()
|
||||
expect(result?.sourceCommandId).toBe(commandId)
|
||||
expect(flushes()).toBe(1)
|
||||
expect(session.events.filter(event => event.type === 'turn/start').at(-1)?.data.turn).toBe(7)
|
||||
expect(session.events.findLast(event => event.type === 'compact/start')?.data)
|
||||
.toEqual({ turn: null })
|
||||
expect(session.events.findLast(event => event.type === 'compact/end')?.data)
|
||||
.toEqual({ turn: null })
|
||||
const start = session.events.findLast(event => event.type === 'compact/start')
|
||||
const summaryEvent = session.events.findLast(event => event.type === 'compact/summary')
|
||||
const checkpoint = session.events.findLast(
|
||||
(event): event is SessionEvent<'user/message'> => event.type === 'user/message'
|
||||
&& isCompactCheckpointSource(event.data.source),
|
||||
)
|
||||
const end = session.events.findLast(event => event.type === 'compact/end')
|
||||
const correlated = { compactionId: result?.compactionId, sourceCommandId: commandId }
|
||||
expect(start?.data).toEqual({ ...correlated, turn: null })
|
||||
expect(summaryEvent?.data.sourceCommandId).toBe(commandId)
|
||||
expect(checkpoint?.data.source).toMatchObject(correlated)
|
||||
expect(end?.data).toEqual({ ...correlated, turn: null })
|
||||
})
|
||||
|
||||
it('reports a live unmatched bracket as busy without summarizing', async () => {
|
||||
const { compact } = detachedService()
|
||||
const session = closedConversation(2)
|
||||
session.append('compact/start', { turn: null })
|
||||
session.append('compact/start', {
|
||||
compactionId: CompactionId('live-manual-compaction'),
|
||||
turn: null,
|
||||
})
|
||||
const agent = fakeAgent(session, () => () => undefined)
|
||||
|
||||
const error = await rejection(() => compact.compactNow(agent, SIGNAL))
|
||||
@@ -421,7 +435,10 @@ describe('compactNow transaction and failure classification', () => {
|
||||
it('ignores an unmatched bracket inherited before a later end-seed marker', async () => {
|
||||
const { compact } = detachedService()
|
||||
const original = closedConversation(2)
|
||||
original.append('compact/start', { turn: null })
|
||||
original.append('compact/start', {
|
||||
compactionId: CompactionId('stale-manual-compaction'),
|
||||
turn: null,
|
||||
})
|
||||
const reloaded = Session.create(SessionId('stale-orphan'), [...original.events])
|
||||
const boundary = reloaded.events.findLast(event => event.type === 'session/end-seed')
|
||||
const orphan = reloaded.events.find(event => event.type === 'compact/start')
|
||||
@@ -435,7 +452,10 @@ describe('compactNow transaction and failure classification', () => {
|
||||
it('scans a stale orphan independently of later repaired turn state', async () => {
|
||||
const { compact } = detachedService()
|
||||
const original = closedConversation(2)
|
||||
original.append('compact/start', { turn: null })
|
||||
original.append('compact/start', {
|
||||
compactionId: CompactionId('reloaded-manual-compaction'),
|
||||
turn: null,
|
||||
})
|
||||
original.append('turn/start', { turn: 3 })
|
||||
original.append('turn/end', { turn: 3, reason: { kind: 'interrupted' } })
|
||||
const reloaded = Session.create(SessionId('reloaded-orphan'), [...original.events])
|
||||
@@ -618,7 +638,7 @@ describe('compactNow transaction and failure classification', () => {
|
||||
const agent = fakeAgent(session, () => () => { released += 1 })
|
||||
const append = session.append.bind(session)
|
||||
vi.spyOn(session, 'append').mockImplementation(((type: string, ...rest: never[]) => {
|
||||
if (type === 'compact/summary') throw new Error('provenance rejected')
|
||||
if (type === 'compact/summary') throw new Error('summary record rejected')
|
||||
return (append as (...args: never[]) => unknown)(type as never, ...rest)
|
||||
}) as never)
|
||||
|
||||
@@ -627,7 +647,7 @@ describe('compactNow transaction and failure classification', () => {
|
||||
expect(error.code).toBe('commit')
|
||||
expect(released).toBe(1)
|
||||
const end = session.events.findLast(event => event.type === 'compact/end')
|
||||
expect(end?.type === 'compact/end' && end.data.error).toContain('provenance rejected')
|
||||
expect(end?.type === 'compact/end' && end.data.error).toContain('summary record rejected')
|
||||
expect(end?.type === 'compact/end' && end.data.turn).toBeNull()
|
||||
})
|
||||
|
||||
@@ -637,14 +657,14 @@ describe('compactNow transaction and failure classification', () => {
|
||||
const agent = fakeAgent(session, () => () => undefined)
|
||||
const append = session.append.bind(session)
|
||||
vi.spyOn(session, 'append').mockImplementation(((type: string, ...rest: never[]) => {
|
||||
if (type === 'compact/summary') throw new Error('provenance rejected')
|
||||
if (type === 'compact/summary') throw new Error('summary record rejected')
|
||||
return (append as (...args: never[]) => unknown)(type as never, ...rest)
|
||||
}) as never)
|
||||
vi.spyOn(ctx.sessions, 'flush').mockRejectedValueOnce(new Error('disk full'))
|
||||
|
||||
const error = await rejection(compact.compactNow(agent, SIGNAL))
|
||||
expect(error.code).toBe('commit')
|
||||
expect(causeOf(error).message).toBe('provenance rejected')
|
||||
expect(causeOf(error).message).toBe('summary record rejected')
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
@@ -664,7 +684,7 @@ describe('compactNow transaction and failure classification', () => {
|
||||
expect(result).not.toBeNull()
|
||||
expect(session.events.some(event => event.type === 'turn/start')).toBe(false)
|
||||
expect(session.events.find(event => event.type === 'compact/start')?.data)
|
||||
.toEqual({ turn: null })
|
||||
.toEqual({ compactionId: result?.compactionId, turn: null })
|
||||
})
|
||||
|
||||
it('classifies a durability failure after the standalone bracket committed', async () => {
|
||||
@@ -676,8 +696,9 @@ describe('compactNow transaction and failure classification', () => {
|
||||
expect((await rejection(compact.compactNow(agent, SIGNAL))).code).toBe('persistence')
|
||||
vi.restoreAllMocks()
|
||||
expect(session.events.some(event => event.type === 'compact/summary')).toBe(true)
|
||||
expect(session.events.findLast(event => event.type === 'compact/end')?.data)
|
||||
.toEqual({ turn: null })
|
||||
const start = session.events.findLast(event => event.type === 'compact/start')
|
||||
const end = session.events.findLast(event => event.type === 'compact/end')
|
||||
expect(end?.data).toEqual({ compactionId: start?.data.compactionId, turn: null })
|
||||
})
|
||||
|
||||
it('lets a pre-aborted signal win before reservation, measurement, or summarization', async () => {
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
{
|
||||
"path": "../../core/agent"
|
||||
},
|
||||
{
|
||||
"path": "../../interaction/commands"
|
||||
},
|
||||
{
|
||||
"path": "../compact"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user