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/core-data-structures/core.i18n.yaml
#	docs/module-graph.md
#	packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx
#	packages/client/ui-conversation/src/client/index.ts
#	packages/compact/compact-basic/README.i18n.yaml
This commit is contained in:
Yichen Jiang
2026-08-08 17:56:53 +08:00
841 files changed
+13085 -5978

No files matched your search

@@ -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/command-compact/README.md
README.md: a32a6aeb9957f0fd5f8cff58b1edbb9bc29a4e3d
README.zh.md: c678f522115d9b0fd414b2f290b3cb54ce690722
README.md: 54f341e39447a423964b7d7435cfb638857eda6e
README.zh.md: d4a122b8a19cdf907212ad019b2528ae52d03886
+1 -1
View File
@@ -12,7 +12,7 @@ Human-facing `/compact` control over [`ctx.compact`](../compact/README.md). The
| `/compact` with no compactable history | `No compactable history yet.` — no marker or surface mutation is written. |
| `/compact <anything>` | `Usage: /compact (no arguments)` — the command takes no arguments and calls no compaction backend. |
The command is backend-independent: it depends only on `compactNow(agent, signal)`. The invoking agent is the exact target, and the dispatching UI's cancellation signal is forwarded through the seam. Every resolved invocation records the executor-owned log-only pair `command/run` / `command/done`; neither event joins model history.
The command is backend-independent: it depends only on `compactNow(agent, signal)`. The invoking agent is the exact target, and the dispatching UI's cancellation signal is forwarded through the seam. Every resolved invocation records the executor-owned log-only pair `command/run` / `command/done`; neither event joins model history. On success, `command/done.sourceEventSeq` names the transaction's `compact/summary` event so a presentation can fold the command lifecycle into its checkpoint without parsing result text or assuming adjacent rows.
Expected `ManualCompactionError` codes become stable direct errors:
@@ -12,7 +12,7 @@
| `/compact`,但没有可压缩历史 | `No compactable history yet.`:不会写入标记,也不会变更 surface。 |
| `/compact <anything>` | `Usage: /compact (no arguments)`:该命令不接受参数,也不会调用压缩后端。 |
该命令与后端无关,只依赖 `compactNow(agent, signal)`。调用该命令的 agent(智能体)就是操作的确切目标,发起分发的 UI 会通过 seam 转发取消信号。每次完成的调用都会记录执行器所属的纯日志事件对 `command/run` / `command/done`;两者都不进入模型历史。
该命令与后端无关,只依赖 `compactNow(agent, signal)`。调用该命令的 agent(智能体)就是操作的确切目标,发起分发的 UI 会通过 seam 转发取消信号。每次完成的调用都会记录执行器所属的纯日志事件对 `command/run` / `command/done`;两者都不进入模型历史。成功时,`command/done.sourceEventSeq` 会指明该事务的 `compact/summary` 事件,让呈现层无须解析结果文本或假定两行相邻,即可将命令生命周期归并到对应检查点中。
预期的 `ManualCompactionError` 代码会成为稳定的直接错误:
@@ -68,6 +68,7 @@ async function executeCompact(
return {
kind: 'success',
text: `Compacted ${result.shadowedSeqs.length} history items (~${result.shadowedTokenCount} tokens).`,
sourceEventSeq: result.summarySeq,
}
} catch (error: unknown) {
if (invocation.signal.aborted) return { kind: 'error', text: 'Compaction cancelled.' }
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import type { Agent } from '@deepseek-ai/dsh-agent'
import CommandService from '@deepseek-ai/dsh-commands'
import CommandService, { type CommandResult } from '@deepseek-ai/dsh-commands'
import {
CompactService,
ManualCompactionError,
@@ -15,9 +15,9 @@ import { Session, SessionId } from '@deepseek-ai/dsh-session'
import * as commandCompact from '@deepseek-ai/dsh-command-compact'
const RESULT: CompactionResult = {
startSeq: 10,
summarySeq: 11,
endSeq: 13,
startSeq: 1,
summarySeq: 2,
endSeq: 3,
summary: [{ type: 'text', text: 'summary' }],
shadowedRange: { start: 1, end: 7 },
shadowedSeqs: [1, 3, 7],
@@ -49,10 +49,24 @@ class StubCompactService extends CompactService {
this.calls.push({ agent, signal })
if (this.operation !== undefined) return this.operation()
return this.failure === undefined
? Promise.resolve(this.result)
? Promise.resolve(this.result === null ? null : this.appendResult(agent, this.result))
// oxlint-disable-next-line typescript/prefer-promise-reject-errors -- exercise arbitrary backend rejection values.
: Promise.reject(this.failure)
}
private appendResult(agent: ManualCompactAgentContext, result: CompactionResult): CompactionResult {
agent.session.append('compact/start', { turn: null })
agent.session.append('compact/summary', {
summary: result.summary,
shadowedRange: result.shadowedRange,
shadowedSeqs: result.shadowedSeqs,
shadowedTokenCount: result.shadowedTokenCount,
provider: 'command-test',
model: 'command-test',
})
agent.session.append('compact/end', { turn: null })
return result
}
}
interface Harness {
@@ -91,9 +105,11 @@ async function run(
function expectLastLifecycle(
test: Harness,
args: string,
outcome: { readonly kind: 'success' | 'error'; readonly text?: string },
outcome: CommandResult,
): string {
const lifecycle = test.agent.session.events.slice(-2)
const lifecycle = test.agent.session.events
.filter(event => event.type === 'command/run' || event.type === 'command/done')
.slice(-2)
const runEvent = lifecycle[0]
const doneEvent = lifecycle[1]
if (runEvent?.type !== 'command/run' || doneEvent?.type !== 'command/done') {
@@ -149,6 +165,7 @@ describe('/compact human command', () => {
expect(execution.result).toEqual({
kind: 'success',
text: 'Compacted 3 history items (~42 tokens).',
sourceEventSeq: RESULT.summarySeq,
})
expect(execution.commandId).toBe(expectLastLifecycle(test, '', execution.result))
expect(test.compact.calls).toEqual([{ agent: test.agent, signal: controller.signal }])
@@ -21,7 +21,7 @@ import { Session, SessionId } from '@deepseek-ai/dsh-session'
const RESULT: CompactionResult = {
startSeq: 1,
summarySeq: 2,
endSeq: 4,
endSeq: 3,
summary: [{ type: 'text', text: 'loader summary' }],
shadowedRange: { start: 3, end: 8 },
shadowedSeqs: [3, 5, 8],
@@ -42,9 +42,19 @@ class LoaderCompactService extends CompactService {
}
override compactNow(
_agent: ManualCompactAgentContext,
agent: ManualCompactAgentContext,
_signal: AbortSignal,
): Promise<CompactionResult | null> {
agent.session.append('compact/start', { turn: null })
agent.session.append('compact/summary', {
summary: RESULT.summary,
shadowedRange: RESULT.shadowedRange,
shadowedSeqs: RESULT.shadowedSeqs,
shadowedTokenCount: RESULT.shadowedTokenCount,
provider: 'loader-test',
model: 'loader-test',
})
agent.session.append('compact/end', { turn: null })
return Promise.resolve(RESULT)
}
}
@@ -108,6 +118,7 @@ describe('command-compact real Loader composition', () => {
expect(execution.result).toEqual({
kind: 'success',
text: 'Compacted 3 history items (~99 tokens).',
sourceEventSeq: RESULT.summarySeq,
})
expect(session.events.map(event => ({ type: event.type, data: event.data }))).toEqual([
{
@@ -119,12 +130,32 @@ describe('command-compact real Loader composition', () => {
source: { kind: 'user' },
},
},
{
type: 'compact/start',
data: { turn: null },
},
{
type: 'compact/summary',
data: {
summary: RESULT.summary,
shadowedRange: RESULT.shadowedRange,
shadowedSeqs: RESULT.shadowedSeqs,
shadowedTokenCount: RESULT.shadowedTokenCount,
provider: 'loader-test',
model: 'loader-test',
},
},
{
type: 'compact/end',
data: { turn: null },
},
{
type: 'command/done',
data: {
commandId: execution.commandId,
kind: 'success',
text: 'Compacted 3 history items (~99 tokens).',
sourceEventSeq: RESULT.summarySeq,
},
},
])
@@ -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: d2a2aeb34210169f1e8dafdc1ad26509b7dd885c
README.zh.md: 14505e443975b1b38bdb9be4fe1961c610c38f02
README.md: 9bea77045c60755ed4280d3239472df456903cc8
README.zh.md: eef454c10b40081655c287815b65f34ae0c5ae47
+1 -1
View File
@@ -21,7 +21,7 @@ This backend owns the compaction policy:
- **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?, provider, model, maxTokens?, usage? }`); 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, 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`.
## Config (`BasicCompactConfig`)
+1 -1
View File
@@ -21,7 +21,7 @@
- **溢出恢复**:提供方已确认的溢出不需容量元数据。它会绕过常规压力与保留,执行剪枝,再尝试一次最大平衡头部缩减,并留下最新不可分单元。只要 `surface.replaceGeneration` 前进,就允许重试,包括剪枝在后续摘要工作抛出异常前已落地的情况。如果没有替换、目标特定上限已耗尽、已取消,或遇到未知/非规范错误,则保留原始提供方失败。
- **失败处理**:活动的未匹配 `compact/start` 是持久锁。位于较新 `session/end-seed` 之前的未匹配标记,是先前生命周期留下的陈旧证据,不会阻塞;位于该边界之后的标记报告 `busy`。摘要和 span 变更失败会以错误闭合,并保持会话表层不变,但日志中仍保留该尝试。闭合失败会有意留下阻塞性的未匹配标记。压力检查中的运行故障会发出警告并继续;只有此前没有替换推进表层时,溢出恢复失败才保留原始提供方错误。完成清理与持久化后,取消仍具有最终决定权。
受保护的 `summarize()` 方法是唯一的子类钩子。基于模板或远程摘要器的子类可以覆盖该方法,同时压力、保留、溯源、缩减验证与已遮蔽 token 计量仍由 `ctx.tokenMeter` 负责。钩子返回安全摘要,以及完整提供方输出、调用 envelope 和可用时的 usage`{ summary, rawOutput?, provider, model, maxTokens?, usage? }`);事务会在 `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`
+5 -3
View File
@@ -43,7 +43,7 @@ interface PreparedCompaction extends SurfaceSelection {
readonly input: SummarizationInput
}
interface SummarizedCompaction extends PreparedCompaction, SummaryResult {
type SummarizedCompaction = PreparedCompaction & SummaryResult & {
readonly checkpointMessage: UserMessage
}
@@ -415,16 +415,18 @@ function commitCompactionBody(
shadowedSeqs,
shadowedTokenCount,
summary,
rawOutput,
provider,
model,
maxTokens,
usage,
checkpointMessage,
} = summarized
const callProvenance = summarized.llmStreamCall === true
? { rawOutput: summarized.rawOutput, llmStreamCall: true as const }
: summarized.rawOutput === undefined ? {} : { rawOutput: summarized.rawOutput }
const summaryEvent = session.append('compact/summary', {
summary,
...rawOutput === undefined ? {} : { rawOutput },
...callProvenance,
shadowedRange: { start, end },
shadowedSeqs: [...shadowedSeqs],
shadowedTokenCount,
@@ -85,16 +85,27 @@ export interface SummarizationInput {
}
/** Safe summary content plus the exact auxiliary call envelope recorded in provenance. */
export interface SummaryResult {
export type SummaryResult = {
summary: ContentBlock[]
/** Complete provider output before the text-only summary projection. */
rawOutput?: ContentBlock[]
provider: string
model: string
maxTokens?: number
/** Provider-reported usage for this summarization request. */
usage?: TokenUsage
}
} & (
| {
/** Complete provider output before the text-only summary projection. */
rawOutput: ContentBlock[]
/** Identifies exactly one call through this context's `ctx.llm.stream()`. */
llmStreamCall: true
}
| {
/** Optional complete output from an unmarked template, remote, or other summarizer. */
rawOutput?: ContentBlock[]
/** An unmarked result does not identify a call through this context's LLM seam. */
llmStreamCall?: never
}
)
/**
* Run the default cache-reusing `ctx.llm.stream()` summarization call: replay
@@ -162,6 +173,7 @@ export async function summarizeWithLlm(
return {
summary,
rawOutput,
llmStreamCall: true,
provider: options.provider,
model: options.model,
maxTokens: config.maxTokens,
@@ -1,10 +1,10 @@
import { describe, expect, it, vi } from 'vitest'
import { describe, expect, expectTypeOf, it, vi } from 'vitest'
import { Context } from 'cordis'
import { AttachmentId } from '@deepseek-ai/dsh-attachment'
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 } from '@deepseek-ai/dsh-compact-basic/src/summarizer.ts'
import type { SummarizationInput, SummaryResult } from '@deepseek-ai/dsh-compact-basic/src/summarizer.ts'
import { toolPairingBalancedAfter, toolPairingBalancedBefore } from '@deepseek-ai/dsh-compact'
import {
resolveCompactSpec,
@@ -869,6 +869,7 @@ describe('compaction region transaction', () => {
rawOutput: compact.rawOutput,
usage: compact.usage,
})
expect(summary?.data).not.toHaveProperty('llmStreamCall')
const head = session.deriveMessages()[0]!
expect(head.content[0]?.type).toBe('text')
expect(head.content[0]?.type === 'text' ? head.content[0].text : '').toContain('<compacted-summary>')
@@ -1166,6 +1167,15 @@ async function summarizerHarness(
}
describe('default one-shot summarizer', () => {
it('requires complete raw output when a subclass marks one local LLM stream call', () => {
expectTypeOf<{
summary: ContentBlock[]
llmStreamCall: true
provider: string
model: string
}>().not.toExtend<SummaryResult>()
})
it('uses configured model/default cap, forwards cancellation, and keeps only safe text', async () => {
const { adapter, compact } = await summarizerHarness([
{ type: 'reasoning', text: 'private' },
@@ -1188,6 +1198,7 @@ describe('default one-shot summarizer', () => {
{ type: 'text', text: 'public summary' },
{ type: 'tool-call', id: CallId('unexpected'), name: 'x', arguments: '{}' },
],
llmStreamCall: true,
provider: MODEL,
model: MODEL,
maxTokens: 321,
@@ -1313,6 +1324,7 @@ describe('default one-shot summarizer', () => {
await compact.compactRegion(nodes[0]!, nodes[3]!, agent(session, MODEL), SIGNAL)
expect(session.events.findLast(event => event.type === 'compact/summary')?.data).toMatchObject({
summary: [{ type: 'text', text: 'routed summary' }],
llmStreamCall: true,
provider: 'routed-summary-provider',
model: 'routed-summary-model',
})
+14 -3
View File
@@ -28,8 +28,6 @@ declare module '@deepseek-ai/dsh-session' {
*/
'compact/summary': {
summary: ContentBlock[]
/** Complete provider output before the backend's safe summary projection. */
rawOutput?: ContentBlock[]
shadowedRange: { start: number; end: number }
shadowedSeqs: number[]
shadowedTokenCount: number
@@ -46,7 +44,20 @@ declare module '@deepseek-ai/dsh-session' {
maxTokens?: number
/** Provider-reported token usage for the summarization request, when emitted. */
usage?: TokenUsage
}
} & (
| {
/** Complete provider output before the backend's safe summary projection. */
rawOutput: ContentBlock[]
/** Identifies exactly one call through this context's `ctx.llm.stream()`. */
llmStreamCall: true
}
| {
/** Optional complete output from an unmarked template, remote, or other summarizer. */
rawOutput?: ContentBlock[]
/** An unmarked summary does not identify a call through this context's LLM seam. */
llmStreamCall?: never
}
)
/**
* Marks the end of a compaction — log-only, releases the lock. Its owner
* matches `compact/start`; `error` records an unsuccessful attempt.