@stackstackstack/dsh-compaction-tool-result-pruner 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +62 -0
- package/README.zh.md +62 -0
- package/lib/index.js +196 -0
- package/lib/invariant.js +20 -0
- package/lib/types/config.d.ts +19 -0
- package/lib/types/index.d.ts +54 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/types.d.ts +37 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/compaction/compaction-tool-result-pruner/README.md
|
|
5
|
+
README.md: 8755f04cfe3e948cfeb67cfb31cbe52168251e15
|
|
6
|
+
README.zh.md: 47c6fec3851b21ea2df9f0d79aa81508530f73dd
|
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @stackstackstack/dsh-compaction-tool-result-pruner
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
The replay-safe model-free pruning service (`ctx.toolResultPruner`). It rewrites over-budget `tool/result` surface nodes to a bounded head, a fixed omission marker, and a bounded tail while retaining the full original event in the append-only session log.
|
|
6
|
+
|
|
7
|
+
This is a concrete companion to [`dsh-compaction-basic`](../compaction-basic/README.md), not a compaction backend or model-facing tool. Compact-basic reads it through optional `ctx.get('toolResultPruner')`, so either package remains independently composable.
|
|
8
|
+
|
|
9
|
+
## Service API
|
|
10
|
+
|
|
11
|
+
`pruneSession(session)` scans one stable snapshot of the current surface. Every over-budget tool result is replaced by one newly appended `tool/result` carrying `{ surfaceOp: { op: 'replace', start: originalSeq, end: originalSeq }, sourceEventSeqs: [originalSeq] }`. The replacement spreads the complete original data and changes only `content`, preserving `turn`, `step`, `callId`, error fields, `meta`, and later data additions. The original event remains available for persistence, replay, and exact-log inspection.
|
|
12
|
+
|
|
13
|
+
The method throws synchronously when the session rejects a replacement. Replacements committed earlier in the pass remain durable.
|
|
14
|
+
|
|
15
|
+
`measureContent(blocks)` counts Unicode code points in `text` blocks. `pruneContent(blocks)` returns the bounded replacement or `null` when content is already within the threshold. Non-text blocks are retained at their original relative positions; text slicing never splits a UTF-16 surrogate pair, though it can split a multi-code-point grapheme cluster.
|
|
16
|
+
|
|
17
|
+
Every emitted result has exactly the configured head budget, fixed marker, and tail budget in text code points, is no larger than `thresholdChars`, and is strictly smaller than the triggering input. A second pass therefore emits no replacement.
|
|
18
|
+
|
|
19
|
+
## Config
|
|
20
|
+
|
|
21
|
+
Unrecognized keys fail at plugin construction. Resolved config is detached and deeply immutable.
|
|
22
|
+
|
|
23
|
+
| Key | Required | Meaning |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `thresholdChars` | no (default `8192`) | Prune when combined text exceeds this many Unicode code points. |
|
|
26
|
+
| `headChars` | no (default `4096`) | Leading Unicode code points retained. |
|
|
27
|
+
| `tailChars` | no (default `1024`) | Trailing Unicode code points retained. |
|
|
28
|
+
|
|
29
|
+
All values are integers; the threshold is positive and head/tail are non-negative. `headChars + marker + tailChars` must fit within `thresholdChars`, so a valid configuration can prune every over-budget result without growth or repeated rewriting.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
35
|
+
import ToolResultPruner from '@stackstackstack/dsh-compaction-tool-result-pruner'
|
|
36
|
+
|
|
37
|
+
export function apply(ctx: Context): void {
|
|
38
|
+
ctx.plugin(ToolResultPruner)
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Model Experience
|
|
43
|
+
|
|
44
|
+
### Pruned tool result
|
|
45
|
+
|
|
46
|
+
#### What the model sees
|
|
47
|
+
|
|
48
|
+
Once a compaction trigger qualifies, future requests see the retained head, `\n\n[... tool result middle pruned ...]\n\n`, and retained tail in place of the removed text. Rich blocks keep their order. The model does not see a second copy of the original.
|
|
49
|
+
|
|
50
|
+
#### Token effect
|
|
51
|
+
|
|
52
|
+
Each rewritten tool result has at most `thresholdChars` text code points. Pruning itself makes no model call; compaction-basic skips summarization when the remeasured request falls below pressure, otherwise the summarizer reads the pruned surface.
|
|
53
|
+
|
|
54
|
+
#### KV Cache effect
|
|
55
|
+
|
|
56
|
+
Replacing an earlier result invalidates reuse from the first changed token. The pruned prefix is eligible for reuse while its route, envelope, and preceding history remain identical.
|
|
57
|
+
|
|
58
|
+
## Known Limitations and Deferred Work
|
|
59
|
+
|
|
60
|
+
- **Character budgets are not token budgets** — provider token density varies, so `ctx.tokenMeter` remains the authority for deciding whether pruning relieved request pressure.
|
|
61
|
+
- **Pruning is syntactic** — it retains the beginning and end without interpreting which middle lines are semantically important.
|
|
62
|
+
- **Grapheme clusters can split** — code-point slicing protects surrogate pairs but does not perform locale-aware grapheme segmentation.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# @stackstackstack/dsh-compaction-tool-result-pruner
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
可安全回放、不依赖模型的剪枝服务(`ctx.toolResultPruner`)。它会将超出预算的 `tool/result` 表层节点改写为长度受限的头部、固定省略标记和长度受限的尾部,同时在仅追加会话日志中保留完整原始事件。
|
|
6
|
+
|
|
7
|
+
这是 [`dsh-compaction-basic`](../compaction-basic/README.md) 的具体配套服务,不是压缩(compaction)后端或面向模型的工具。Compact-basic 通过可选的 `ctx.get('toolResultPruner')` 读取它,因此这两个包仍可各自独立组合。
|
|
8
|
+
|
|
9
|
+
## 服务 API
|
|
10
|
+
|
|
11
|
+
`pruneSession(session)` 会扫描当前表层的一个稳定快照。每个超出预算的工具结果都会被一个新追加的 `tool/result` 替换,其携带 `{ surfaceOp: { op: 'replace', start: originalSeq, end: originalSeq }, sourceEventSeqs: [originalSeq] }`。替换会展开完整原始数据,只更改 `content`,保留 `turn`、`step`、`callId`、错误字段、`meta` 以及以后新增的数据字段。原始事件仍可用于持久化、回放和精确日志检查。
|
|
12
|
+
|
|
13
|
+
当会话拒绝替换时,该方法会同步抛出异常。本次扫描中先前已提交的替换仍会保留。
|
|
14
|
+
|
|
15
|
+
`measureContent(blocks)` 会统计 `text` 块中的 Unicode 码点。`pruneContent(blocks)` 会返回长度受限的替换;如果内容已在阈值内,则返回 `null`。非文本块保持原始相对位置;文本切片绝不会拆分 UTF-16 代理项对,但可能拆分由多个码点组成的字素簇。
|
|
16
|
+
|
|
17
|
+
每个发出的结果在文本码点上都精确包含已配置的头部预算、固定标记和尾部预算,不大于 `thresholdChars`,且严格小于触发输入。因此第二次扫描不会发出替换。
|
|
18
|
+
|
|
19
|
+
## 配置
|
|
20
|
+
|
|
21
|
+
无法识别的配置键会使插件在构造时失败。已解析配置与输入脱离,并且深度不可变。
|
|
22
|
+
|
|
23
|
+
| 配置键 | 必填 | 含义 |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| `thresholdChars` | 否(默认 `8192`) | 合并文本超过此 Unicode 码点数时剪枝。 |
|
|
26
|
+
| `headChars` | 否(默认 `4096`) | 保留的开头 Unicode 码点数。 |
|
|
27
|
+
| `tailChars` | 否(默认 `1024`) | 保留的末尾 Unicode 码点数。 |
|
|
28
|
+
|
|
29
|
+
所有值都必须是整数;阈值必须为正数,头部/尾部必须为非负数。`headChars + marker + tailChars` 之和不得超过 `thresholdChars`,因此有效配置可以剪枝每个超出预算的结果,不会增长或重复改写。
|
|
30
|
+
|
|
31
|
+
## 用法
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
35
|
+
import ToolResultPruner from '@stackstackstack/dsh-compaction-tool-result-pruner'
|
|
36
|
+
|
|
37
|
+
export function apply(ctx: Context): void {
|
|
38
|
+
ctx.plugin(ToolResultPruner)
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 模型体验
|
|
43
|
+
|
|
44
|
+
### 已剪枝的工具结果
|
|
45
|
+
|
|
46
|
+
#### 模型看到的内容
|
|
47
|
+
|
|
48
|
+
一旦满足压缩触发条件,后续请求看到的将是保留的头部、`\n\n[... tool result middle pruned ...]\n\n` 和保留的尾部,而非被移除的文本。非文本块保持原有顺序。模型不会看到原文的第二份副本。
|
|
49
|
+
|
|
50
|
+
#### Token 影响
|
|
51
|
+
|
|
52
|
+
每个已改写工具结果最多包含 `thresholdChars` 个文本码点。剪枝本身不会发起模型调用;重新测量的请求低于压力阈值时,compaction-basic 会跳过摘要,否则摘要器会读取已剪枝的表层。
|
|
53
|
+
|
|
54
|
+
#### KV Cache 影响
|
|
55
|
+
|
|
56
|
+
替换较早的结果会使从第一个改变的 token 起的复用失效。当其路由、envelope 与之前的历史保持一致时,已剪枝前缀可以复用。
|
|
57
|
+
|
|
58
|
+
## 已知限制与暂缓事项
|
|
59
|
+
|
|
60
|
+
- **字符预算不是 token 预算**:不同提供方的 token 密度各异,因此 `ctx.tokenMeter` 仍负责判定剪枝是否缓解了请求压力。
|
|
61
|
+
- **剪枝只基于语法**:它保留开头与结尾,不解释中间哪些行在语义上重要。
|
|
62
|
+
- **字素簇可能被拆分**:按码点切片可保护代理项对,但不会执行感知区域设置的字素簇分割。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { deepFreeze, freezeMessage } from "@stackstackstack/dsh-llm";
|
|
4
|
+
//#region lib/types/config.js
|
|
5
|
+
/** Configuration resolution for deterministic tool-result pruning. */
|
|
6
|
+
/** Fixed marker substituted for every removed middle span. */
|
|
7
|
+
const PRUNE_MARKER = "\n\n[... tool result middle pruned ...]\n\n";
|
|
8
|
+
/** Low-friction defaults for coding-agent tool output. */
|
|
9
|
+
const DEFAULTS = deepFreeze({
|
|
10
|
+
thresholdChars: 8192,
|
|
11
|
+
headChars: 4096,
|
|
12
|
+
tailChars: 1024
|
|
13
|
+
});
|
|
14
|
+
const CONFIG_KEYS = new Set([
|
|
15
|
+
"thresholdChars",
|
|
16
|
+
"headChars",
|
|
17
|
+
"tailChars"
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Count Unicode code points without splitting surrogate pairs.
|
|
21
|
+
* @param text - text to measure.
|
|
22
|
+
* @returns the Unicode code-point count.
|
|
23
|
+
*/
|
|
24
|
+
function codePointLength(text) {
|
|
25
|
+
return Array.from(text).length;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve and validate pruning budgets.
|
|
29
|
+
* @param config - raw plugin configuration.
|
|
30
|
+
* @returns a detached deeply immutable configuration.
|
|
31
|
+
*/
|
|
32
|
+
function resolveConfig(config = {}) {
|
|
33
|
+
for (const key of Object.keys(config)) if (!CONFIG_KEYS.has(key)) throw new Error(`ToolResultPruneConfig: unknown key "${key}" (allowed: thresholdChars, headChars, tailChars)`);
|
|
34
|
+
const resolved = {
|
|
35
|
+
thresholdChars: config.thresholdChars ?? DEFAULTS.thresholdChars,
|
|
36
|
+
headChars: config.headChars ?? DEFAULTS.headChars,
|
|
37
|
+
tailChars: config.tailChars ?? DEFAULTS.tailChars
|
|
38
|
+
};
|
|
39
|
+
assertPositiveInteger("thresholdChars", resolved.thresholdChars);
|
|
40
|
+
assertNonNegativeInteger("headChars", resolved.headChars);
|
|
41
|
+
assertNonNegativeInteger("tailChars", resolved.tailChars);
|
|
42
|
+
const emittedChars = resolved.headChars + codePointLength(PRUNE_MARKER) + resolved.tailChars;
|
|
43
|
+
if (emittedChars > resolved.thresholdChars) throw new Error(`ToolResultPruneConfig: headChars + marker + tailChars (${emittedChars}) must be at most thresholdChars (${resolved.thresholdChars})`);
|
|
44
|
+
return deepFreeze(structuredClone(resolved));
|
|
45
|
+
}
|
|
46
|
+
function assertPositiveInteger(name, value) {
|
|
47
|
+
if (!Number.isInteger(value) || value <= 0) throw new Error(`ToolResultPruneConfig: ${name} (${value}) must be a positive integer`);
|
|
48
|
+
}
|
|
49
|
+
function assertNonNegativeInteger(name, value) {
|
|
50
|
+
if (!Number.isInteger(value) || value < 0) throw new Error(`ToolResultPruneConfig: ${name} (${value}) must be a non-negative integer`);
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region lib/types/index.js
|
|
54
|
+
/**
|
|
55
|
+
* Replay-safe, model-free tool-result pruning service.
|
|
56
|
+
*
|
|
57
|
+
* @module @stackstackstack/dsh-compaction-tool-result-pruner
|
|
58
|
+
*/
|
|
59
|
+
/** Deterministic head/middle/tail pruning for current tool-result surface nodes. */
|
|
60
|
+
var ToolResultPruner = class extends Service {
|
|
61
|
+
static inject = ["tokenMeter"];
|
|
62
|
+
static Config = z.object({
|
|
63
|
+
thresholdChars: z.number().step(1).min(1).default(DEFAULTS.thresholdChars),
|
|
64
|
+
headChars: z.number().step(1).min(0).default(DEFAULTS.headChars),
|
|
65
|
+
tailChars: z.number().step(1).min(0).default(DEFAULTS.tailChars)
|
|
66
|
+
});
|
|
67
|
+
/** Resolved and immutable character budgets. */
|
|
68
|
+
config;
|
|
69
|
+
constructor(ctx, config = {}) {
|
|
70
|
+
super(ctx, "toolResultPruner");
|
|
71
|
+
this.config = resolveConfig(config);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Measure text content in Unicode code points; non-text blocks cost zero.
|
|
75
|
+
* @param blocks - tool-result content to measure.
|
|
76
|
+
* @returns total Unicode code points across text blocks.
|
|
77
|
+
*/
|
|
78
|
+
measureContent(blocks) {
|
|
79
|
+
let chars = 0;
|
|
80
|
+
for (const block of blocks) if (block.type === "text") chars += codePointLength(block.text);
|
|
81
|
+
return chars;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Replace an over-budget text middle while retaining rich-block order.
|
|
85
|
+
* Text slicing is by Unicode code point, not UTF-16 code unit, so a retained
|
|
86
|
+
* boundary cannot split a surrogate pair. Grapheme clusters may still split.
|
|
87
|
+
* @param blocks - original tool-result content.
|
|
88
|
+
* @returns pruned content, or `null` when the text is within budget.
|
|
89
|
+
*/
|
|
90
|
+
pruneContent(blocks) {
|
|
91
|
+
const totalChars = this.measureContent(blocks);
|
|
92
|
+
if (totalChars <= this.config.thresholdChars) return null;
|
|
93
|
+
const removedStart = this.config.headChars;
|
|
94
|
+
const removedEnd = totalChars - this.config.tailChars;
|
|
95
|
+
const pruned = [];
|
|
96
|
+
let consumed = 0;
|
|
97
|
+
let markerInserted = false;
|
|
98
|
+
for (const block of blocks) {
|
|
99
|
+
if (block.type !== "text") {
|
|
100
|
+
pruned.push(block);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const points = Array.from(block.text);
|
|
104
|
+
const blockStart = consumed;
|
|
105
|
+
const blockEnd = blockStart + points.length;
|
|
106
|
+
const headEnd = Math.min(points.length, Math.max(0, removedStart - blockStart));
|
|
107
|
+
const tailStart = Math.min(points.length, Math.max(0, removedEnd - blockStart));
|
|
108
|
+
const marker = blockStart < removedEnd && blockEnd > removedStart && !markerInserted ? PRUNE_MARKER : "";
|
|
109
|
+
if (marker.length > 0) markerInserted = true;
|
|
110
|
+
const text = points.slice(0, headEnd).join("") + marker + points.slice(tailStart).join("");
|
|
111
|
+
if (text.length > 0) pruned.push({
|
|
112
|
+
...block,
|
|
113
|
+
text
|
|
114
|
+
});
|
|
115
|
+
consumed = blockEnd;
|
|
116
|
+
}
|
|
117
|
+
/* v8 ignore next -- totalChars > threshold and valid budgets guarantee a removed text span. */
|
|
118
|
+
if (!markerInserted) throw new Error("tool-result prune: failed to locate the removed text span");
|
|
119
|
+
const charsAfter = this.measureContent(pruned);
|
|
120
|
+
/* v8 ignore next -- config validation fixes the emitted head + marker + tail budget. */
|
|
121
|
+
if (charsAfter > this.config.thresholdChars || charsAfter >= totalChars) throw new Error("tool-result prune: replacement must be smaller and within threshold");
|
|
122
|
+
return pruned;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Prune every over-budget tool result from one stable current-surface snapshot.
|
|
126
|
+
* Each replacement preserves the complete event data except for `content`,
|
|
127
|
+
* cites the shadowed node so replay can recover the replacement input, and is
|
|
128
|
+
* immediately preceded by a `compaction/prune` shadow-price event pricing the
|
|
129
|
+
* shadowed node through the injected token meter, so pure consumers can
|
|
130
|
+
* subtract it without per-node state.
|
|
131
|
+
* @param session - session whose current surface is rewritten.
|
|
132
|
+
* @returns landed replacements and aggregate Unicode-code-point savings.
|
|
133
|
+
* @throws when the session rejects a replacement; replacements committed
|
|
134
|
+
* earlier in the pass remain durable.
|
|
135
|
+
*/
|
|
136
|
+
pruneSession(session) {
|
|
137
|
+
const candidates = [];
|
|
138
|
+
for (const seq of [...session.surface.nodes]) {
|
|
139
|
+
const event = session.events[seq];
|
|
140
|
+
/* v8 ignore next -- surface seqs are validated contiguous log references. */
|
|
141
|
+
if (event?.type === "tool/result") candidates.push({
|
|
142
|
+
seq,
|
|
143
|
+
event
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const pruned = [];
|
|
147
|
+
let charsRemoved = 0;
|
|
148
|
+
for (const { seq, event } of candidates) {
|
|
149
|
+
const result = event.data.message.content[0];
|
|
150
|
+
const content = this.pruneContent(result.content);
|
|
151
|
+
if (content === null) continue;
|
|
152
|
+
const charsBefore = this.measureContent(result.content);
|
|
153
|
+
const charsAfter = this.measureContent(content);
|
|
154
|
+
const message = freezeMessage({
|
|
155
|
+
...event.data.message,
|
|
156
|
+
content: [{
|
|
157
|
+
...result,
|
|
158
|
+
content
|
|
159
|
+
}]
|
|
160
|
+
});
|
|
161
|
+
session.append("compaction/prune", {
|
|
162
|
+
shadowedRange: {
|
|
163
|
+
start: seq,
|
|
164
|
+
end: seq
|
|
165
|
+
},
|
|
166
|
+
shadowedSeqs: [seq],
|
|
167
|
+
shadowedTokenCount: this.ctx.tokenMeter.estimateMessage(event.data.message)
|
|
168
|
+
});
|
|
169
|
+
const replacement = session.append("tool/result", {
|
|
170
|
+
...event.data,
|
|
171
|
+
message
|
|
172
|
+
}, {
|
|
173
|
+
surfaceOp: {
|
|
174
|
+
op: "replace",
|
|
175
|
+
start: seq,
|
|
176
|
+
end: seq
|
|
177
|
+
},
|
|
178
|
+
sourceEventSeqs: [seq]
|
|
179
|
+
});
|
|
180
|
+
pruned.push({
|
|
181
|
+
originalSeq: seq,
|
|
182
|
+
replacementSeq: replacement.seq,
|
|
183
|
+
callId: event.data.message.source.callId,
|
|
184
|
+
charsBefore,
|
|
185
|
+
charsAfter
|
|
186
|
+
});
|
|
187
|
+
charsRemoved += charsBefore - charsAfter;
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
pruned,
|
|
191
|
+
charsRemoved
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
//#endregion
|
|
196
|
+
export { DEFAULTS, PRUNE_MARKER, ToolResultPruner, ToolResultPruner as default, codePointLength, resolveConfig };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@stackstackstack/dsh-compaction-tool-result-pruner`.
|
|
4
|
+
* @module @stackstackstack/dsh-compaction-tool-result-pruner/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@stackstackstack/dsh-compaction-tool-result-pruner";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "compaction-tool-result-pruner-invariant";
|
|
9
|
+
/** Services required before the companion can register. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/** No runtime invariant: Session validates each content-only rewrite and its companion owns cross-event enclosure. */
|
|
12
|
+
const install = () => {};
|
|
13
|
+
/**
|
|
14
|
+
* Register this package's invariant companion.
|
|
15
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
16
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
17
|
+
*/
|
|
18
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
19
|
+
//#endregion
|
|
20
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Configuration resolution for deterministic tool-result pruning. */
|
|
2
|
+
import type { ResolvedConfig, ToolResultPruneConfig } from './types.ts';
|
|
3
|
+
/** Fixed marker substituted for every removed middle span. */
|
|
4
|
+
export declare const PRUNE_MARKER = "\n\n[... tool result middle pruned ...]\n\n";
|
|
5
|
+
/** Low-friction defaults for coding-agent tool output. */
|
|
6
|
+
export declare const DEFAULTS: ResolvedConfig;
|
|
7
|
+
/**
|
|
8
|
+
* Count Unicode code points without splitting surrogate pairs.
|
|
9
|
+
* @param text - text to measure.
|
|
10
|
+
* @returns the Unicode code-point count.
|
|
11
|
+
*/
|
|
12
|
+
export declare function codePointLength(text: string): number;
|
|
13
|
+
/**
|
|
14
|
+
* Resolve and validate pruning budgets.
|
|
15
|
+
* @param config - raw plugin configuration.
|
|
16
|
+
* @returns a detached deeply immutable configuration.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveConfig(config?: ToolResultPruneConfig): ResolvedConfig;
|
|
19
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay-safe, model-free tool-result pruning service.
|
|
3
|
+
*
|
|
4
|
+
* @module @stackstackstack/dsh-compaction-tool-result-pruner
|
|
5
|
+
*/
|
|
6
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
7
|
+
import z from '@deepseek-ai/schemastery';
|
|
8
|
+
import type { ContentBlock } from '@stackstackstack/dsh-llm';
|
|
9
|
+
import type { Session } from '@stackstackstack/dsh-session';
|
|
10
|
+
import type { PruneResult, ResolvedConfig, ToolResultPruneConfig } from './types.ts';
|
|
11
|
+
export { codePointLength, DEFAULTS, PRUNE_MARKER, resolveConfig } from './config.ts';
|
|
12
|
+
export type { PrunedEntry, PruneResult, ResolvedConfig, ToolResultPruneConfig, } from './types.ts';
|
|
13
|
+
declare module '@deepseek-ai/cordis' {
|
|
14
|
+
interface Context {
|
|
15
|
+
toolResultPruner: ToolResultPruner;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Deterministic head/middle/tail pruning for current tool-result surface nodes. */
|
|
19
|
+
export declare class ToolResultPruner extends Service {
|
|
20
|
+
static inject: string[];
|
|
21
|
+
static Config: z<ToolResultPruneConfig>;
|
|
22
|
+
/** Resolved and immutable character budgets. */
|
|
23
|
+
readonly config: ResolvedConfig;
|
|
24
|
+
constructor(ctx: Context, config?: ToolResultPruneConfig);
|
|
25
|
+
/**
|
|
26
|
+
* Measure text content in Unicode code points; non-text blocks cost zero.
|
|
27
|
+
* @param blocks - tool-result content to measure.
|
|
28
|
+
* @returns total Unicode code points across text blocks.
|
|
29
|
+
*/
|
|
30
|
+
measureContent(blocks: readonly ContentBlock[]): number;
|
|
31
|
+
/**
|
|
32
|
+
* Replace an over-budget text middle while retaining rich-block order.
|
|
33
|
+
* Text slicing is by Unicode code point, not UTF-16 code unit, so a retained
|
|
34
|
+
* boundary cannot split a surrogate pair. Grapheme clusters may still split.
|
|
35
|
+
* @param blocks - original tool-result content.
|
|
36
|
+
* @returns pruned content, or `null` when the text is within budget.
|
|
37
|
+
*/
|
|
38
|
+
pruneContent(blocks: readonly ContentBlock[]): ContentBlock[] | null;
|
|
39
|
+
/**
|
|
40
|
+
* Prune every over-budget tool result from one stable current-surface snapshot.
|
|
41
|
+
* Each replacement preserves the complete event data except for `content`,
|
|
42
|
+
* cites the shadowed node so replay can recover the replacement input, and is
|
|
43
|
+
* immediately preceded by a `compaction/prune` shadow-price event pricing the
|
|
44
|
+
* shadowed node through the injected token meter, so pure consumers can
|
|
45
|
+
* subtract it without per-node state.
|
|
46
|
+
* @param session - session whose current surface is rewritten.
|
|
47
|
+
* @returns landed replacements and aggregate Unicode-code-point savings.
|
|
48
|
+
* @throws when the session rejects a replacement; replacements committed
|
|
49
|
+
* earlier in the pass remain durable.
|
|
50
|
+
*/
|
|
51
|
+
pruneSession(session: Session): PruneResult;
|
|
52
|
+
}
|
|
53
|
+
export default ToolResultPruner;
|
|
54
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned invariant companion for `@stackstackstack/dsh-compaction-tool-result-pruner`.
|
|
3
|
+
* @module @stackstackstack/dsh-compaction-tool-result-pruner/invariant
|
|
4
|
+
*/
|
|
5
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
export declare const name = "compaction-tool-result-pruner-invariant";
|
|
8
|
+
/** Services required before the companion can register. */
|
|
9
|
+
export declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
13
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
14
|
+
*/
|
|
15
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
16
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CallId } from '@stackstackstack/dsh-llm';
|
|
2
|
+
/** Character-budget policy for deterministic tool-result pruning. */
|
|
3
|
+
export interface ToolResultPruneConfig {
|
|
4
|
+
/** Prune when total text exceeds this many Unicode code points. Defaults to `8192`. */
|
|
5
|
+
thresholdChars?: number;
|
|
6
|
+
/** Maximum leading Unicode code points retained. Defaults to `4096`. */
|
|
7
|
+
headChars?: number;
|
|
8
|
+
/** Maximum trailing Unicode code points retained. Defaults to `1024`. */
|
|
9
|
+
tailChars?: number;
|
|
10
|
+
}
|
|
11
|
+
/** Validated, detached, deeply immutable pruning configuration. */
|
|
12
|
+
export interface ResolvedConfig {
|
|
13
|
+
readonly thresholdChars: number;
|
|
14
|
+
readonly headChars: number;
|
|
15
|
+
readonly tailChars: number;
|
|
16
|
+
}
|
|
17
|
+
/** Cited source event and size accounting for one landed surface replacement. */
|
|
18
|
+
export interface PrunedEntry {
|
|
19
|
+
/** Full-fidelity tool-result event shadowed by the replacement. */
|
|
20
|
+
readonly originalSeq: number;
|
|
21
|
+
/** Newly appended pruned tool-result event. */
|
|
22
|
+
readonly replacementSeq: number;
|
|
23
|
+
/** Tool call shared by the original and replacement. */
|
|
24
|
+
readonly callId: CallId;
|
|
25
|
+
/** Original text size in Unicode code points. */
|
|
26
|
+
readonly charsBefore: number;
|
|
27
|
+
/** Replacement text size in Unicode code points. */
|
|
28
|
+
readonly charsAfter: number;
|
|
29
|
+
}
|
|
30
|
+
/** Aggregate outcome of one stable-surface pruning pass. */
|
|
31
|
+
export interface PruneResult {
|
|
32
|
+
/** Replacements in the snapshotted surface order. */
|
|
33
|
+
readonly pruned: readonly PrunedEntry[];
|
|
34
|
+
/** Total Unicode code points removed across replacements. */
|
|
35
|
+
readonly charsRemoved: number;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=types.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stackstackstack/dsh-compaction-tool-result-pruner",
|
|
3
|
+
"description": "Replay-safe model-free head/middle/tail pruning for tool-result surface nodes",
|
|
4
|
+
"version": "0.1.5",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/compaction/compaction-tool-result-pruner"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@stackstackstack/dsh-invariants": "^0.1.5",
|
|
36
|
+
"@stackstackstack/dsh-compaction": "^0.1.5",
|
|
37
|
+
"@stackstackstack/dsh-session": "^0.1.5",
|
|
38
|
+
"@stackstackstack/dsh-llm": "^0.1.5",
|
|
39
|
+
"@stackstackstack/dsh-token-meter": "^0.1.5",
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@deepseek-ai/cordis-plugin-include": "^1.0.6",
|
|
47
|
+
"@stackstackstack/dsh-compaction": "^0.1.5",
|
|
48
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.2",
|
|
49
|
+
"@stackstackstack/dsh-invariants": "^0.1.5",
|
|
50
|
+
"@stackstackstack/dsh-token-meter": "^0.1.5",
|
|
51
|
+
"@stackstackstack/dsh-session": "^0.1.5",
|
|
52
|
+
"@stackstackstack/dsh-llm": "^0.1.5",
|
|
53
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
54
|
+
}
|
|
55
|
+
}
|