Files
deepseek-harness/packages/compact/compact-tool-result-prune/src/index.ts
T
imccyu ec601ca13d build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.

Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.

The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.

Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:13 +08:00

188 lines
7.1 KiB
TypeScript

/**
* Replay-safe, model-free tool-result pruning service.
*
* @module @deepseek-ai/dsh-compact-tool-result-prune
*/
import { Context, Service } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { freezeMessage } from '@deepseek-ai/dsh-llm'
import type { ContentBlock } from '@deepseek-ai/dsh-llm'
import type { Session, SessionEvent, ToolResultMessage } from '@deepseek-ai/dsh-session'
// Type-only: the `compact/*` SessionEventMap merges (the shadow-price event).
import type {} from '@deepseek-ai/dsh-compact'
// Type-only: the `ctx.tokenMeter` Context merge for the declared injection.
import type {} from '@deepseek-ai/dsh-token-meter'
import { codePointLength, DEFAULTS, PRUNE_MARKER, resolveConfig } from './config.ts'
import type {
PrunedEntry,
PruneResult,
ResolvedConfig,
ToolResultPruneConfig,
} from './types.ts'
export { codePointLength, DEFAULTS, PRUNE_MARKER, resolveConfig } from './config.ts'
export type {
PrunedEntry,
PruneResult,
ResolvedConfig,
ToolResultPruneConfig,
} from './types.ts'
declare module '@deepseek-ai/cordis' {
interface Context {
toolResultPrune: ToolResultPruneService
}
}
interface SnapshotCandidate {
readonly seq: number
readonly event: SessionEvent<'tool/result'>
}
/** Deterministic head/middle/tail pruning for current tool-result surface nodes. */
export class ToolResultPruneService extends Service {
// The token meter prices each shadowed node for its logged shadow-price
// event, so pruning genuinely requires the pricing capability.
static inject = ['tokenMeter']
static Config: z<ToolResultPruneConfig> = z.object({
thresholdChars: z.number().step(1).min(1).default(DEFAULTS.thresholdChars),
headChars: z.number().step(1).min(0).default(DEFAULTS.headChars),
tailChars: z.number().step(1).min(0).default(DEFAULTS.tailChars),
})
/** Resolved and immutable character budgets. */
readonly config: ResolvedConfig
constructor(ctx: Context, config: ToolResultPruneConfig = {}) {
super(ctx, 'toolResultPrune')
this.config = resolveConfig(config)
}
/**
* Measure text content in Unicode code points; non-text blocks cost zero.
* @param blocks - tool-result content to measure.
* @returns total Unicode code points across text blocks.
*/
measureContent(blocks: readonly ContentBlock[]): number {
let chars = 0
for (const block of blocks) {
if (block.type === 'text') chars += codePointLength(block.text)
}
return chars
}
/**
* Replace an over-budget text middle while retaining rich-block order.
* Text slicing is by Unicode code point, not UTF-16 code unit, so a retained
* boundary cannot split a surrogate pair. Grapheme clusters may still split.
* @param blocks - original tool-result content.
* @returns pruned content, or `null` when the text is within budget.
*/
pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null {
const totalChars = this.measureContent(blocks)
if (totalChars <= this.config.thresholdChars) return null
const removedStart = this.config.headChars
const removedEnd = totalChars - this.config.tailChars
const pruned: ContentBlock[] = []
let consumed = 0
let markerInserted = false
for (const block of blocks) {
if (block.type !== 'text') {
pruned.push(block)
continue
}
const points = Array.from(block.text)
const blockStart = consumed
const blockEnd = blockStart + points.length
const headEnd = Math.min(points.length, Math.max(0, removedStart - blockStart))
const tailStart = Math.min(points.length, Math.max(0, removedEnd - blockStart))
const intersectsRemoved = blockStart < removedEnd && blockEnd > removedStart
const marker = intersectsRemoved && !markerInserted ? PRUNE_MARKER : ''
if (marker.length > 0) markerInserted = true
const text = points.slice(0, headEnd).join('')
+ marker
+ points.slice(tailStart).join('')
if (text.length > 0) pruned.push({ ...block, text })
consumed = blockEnd
}
/* v8 ignore next -- totalChars > threshold and valid budgets guarantee a removed text span. */
if (!markerInserted) throw new Error('tool-result prune: failed to locate the removed text span')
const charsAfter = this.measureContent(pruned)
/* v8 ignore next -- config validation fixes the emitted head + marker + tail budget. */
if (charsAfter > this.config.thresholdChars || charsAfter >= totalChars) {
throw new Error('tool-result prune: replacement must be smaller and within threshold')
}
return pruned
}
/**
* Prune every over-budget tool result from one stable current-surface snapshot.
* Each replacement preserves the complete event data except for `content`,
* cites the shadowed node so replay can recover the replacement input, and is
* immediately preceded by a `compact/prune` shadow-price event pricing the
* shadowed node through the injected token meter, so pure consumers can
* subtract it without per-node state.
* @param session - session whose current surface is rewritten.
* @returns landed replacements and aggregate Unicode-code-point savings.
* @throws when the session rejects a replacement; replacements committed
* earlier in the pass remain durable.
*/
pruneSession(session: Session): PruneResult {
const candidates: SnapshotCandidate[] = []
for (const seq of [...session.surface.nodes]) {
const event = session.events[seq]
/* v8 ignore next -- surface seqs are validated contiguous log references. */
if (event?.type === 'tool/result') candidates.push({ seq, event })
}
const pruned: PrunedEntry[] = []
let charsRemoved = 0
for (const { seq, event } of candidates) {
const result = event.data.message.content[0]
const content = this.pruneContent(result.content)
if (content === null) continue
const charsBefore = this.measureContent(result.content)
const charsAfter = this.measureContent(content)
const message = freezeMessage<ToolResultMessage>({
...event.data.message,
content: [{
...result,
content,
}] as [typeof result],
})
// Shadow-price protocol: the metering event and its replacement are
// appended synchronously adjacent, so pure consumers subtract the
// shadowed node's heuristic price without retaining per-node state.
session.append('compact/prune', {
shadowedRange: { start: seq, end: seq },
shadowedSeqs: [seq],
shadowedTokenCount: this.ctx.tokenMeter.estimateMessage(event.data.message),
})
const replacement = session.append('tool/result', {
...event.data,
message,
}, {
surfaceOp: { op: 'replace', start: seq, end: seq },
sourceEventSeqs: [seq],
})
pruned.push({
originalSeq: seq,
replacementSeq: replacement.seq,
callId: event.data.message.source.callId,
charsBefore,
charsAfter,
})
charsRemoved += charsBefore - charsAfter
}
return { pruned, charsRemoved }
}
}
export default ToolResultPruneService