dsh-context-opencode 0.1.1
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.md +51 -0
- package/README.zh-CN.md +51 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline.d.ts +4 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +236 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/strategy.d.ts +5 -0
- package/dist/strategy.d.ts.map +1 -0
- package/dist/strategy.js +46 -0
- package/dist/strategy.js.map +1 -0
- package/package.json +60 -0
- package/src/index.ts +6 -0
- package/src/pipeline.ts +205 -0
- package/src/strategy.ts +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-context-zoo contributors
|
|
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.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# OpenCode Context Strategy
|
|
2
|
+
|
|
3
|
+
English | [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
This package adapts the OpenCode workflow for usage accounting, tool-output pruning, retained-history selection, summary requests, result validation, and overflow recovery. DSH reads the log, calls the model, and commits replacements.
|
|
6
|
+
|
|
7
|
+
## npm installation
|
|
8
|
+
|
|
9
|
+
After release, install with `dsh plugin --profile web add dsh-context-opencode`. Apply the host Session patch and generate the activation overlay as described in the [npm guide](https://github.com/zonemeen/dsh-context-zoo/blob/main/docs/publishing.md#using-the-published-packages). Installing the package alone does not replace the active context engine.
|
|
10
|
+
|
|
11
|
+
## Context flow
|
|
12
|
+
|
|
13
|
+
Optional tool-output pruning runs before the automatic threshold check. Recent turns stay outside the summary replacement; on overflow, the latest user message is also retained when earlier user history exists. Transcript clipping only affects summary input; original messages remain in the session log.
|
|
14
|
+
|
|
15
|
+
```mermaid
|
|
16
|
+
flowchart TD
|
|
17
|
+
A["Automatic check, manual compact,<br/>or overflow recovery"] --> B{"Within failure and recovery limits?"}
|
|
18
|
+
B -->|No| L["Keep the current context"]
|
|
19
|
+
B -->|Yes| C["Optional tool-output pruning<br/>Disabled by default"]
|
|
20
|
+
C --> D{"Automatic threshold met<br/>or manual / overflow trigger?"}
|
|
21
|
+
D -->|No| E["Keep the current context<br/>Any completed pruning remains"]
|
|
22
|
+
D -->|Yes| F["Select older history<br/>Keep recent turns and tool pairs intact"]
|
|
23
|
+
F --> G{"History available to summarize?"}
|
|
24
|
+
G -->|No| E
|
|
25
|
+
G -->|Yes| H["Summarize selected history<br/>Merge prior checkpoints"]
|
|
26
|
+
H --> I{"Summary and replacement valid?"}
|
|
27
|
+
I -->|Yes| J["Commit checkpoint with retained tail<br/>On overflow, describe retained media"]
|
|
28
|
+
I -->|No| K["Do not replace history with a summary<br/>Any completed pruning remains"]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Default behavior
|
|
32
|
+
|
|
33
|
+
- Uses the provider's total usage when available, or sums input, output, and cache usage. Without usage data, it estimates tokens by dividing the message JSON character count by 4 after removing DSH identity fields.
|
|
34
|
+
- When the model has a separate input limit, subtracts `reserveTokens` from that limit; the default reserve is the smaller of the output budget and 20,000 tokens. With only a total context window, subtracts the output budget, which defaults to 32,000 tokens when unavailable.
|
|
35
|
+
- Triggers when usage reaches the threshold. The retention budget is 25% of the usable window, with a minimum of 2,000 and a maximum of 15,000 tokens. Retains complete recent user turns first; when a turn does not fit, finds a suffix that fits while keeping tool calls and results together.
|
|
36
|
+
- Tool-output pruning is disabled by default. When enabled, scans backward from the newest messages and processes older results only after passing the second user message. Protects the first 40,000 tokens of accumulated tool output and the `skill` tool. Stops at a previous summary or a cleared result. Clears candidate outputs only when their total strictly exceeds 20,000 tokens; error results are retained.
|
|
37
|
+
- Sends a separately serialized transcript for summarization, preserving tool names and arguments. Each tool output is limited to 2,000 characters by default. Merges the previous summary using the headings Objective, Important Details, Work State, Next Move, and Relevant Files.
|
|
38
|
+
- Limits summary output to the smaller of the current output budget and 32,000 tokens by default. Rejects blank, truncated, error, tool-call, and media responses. A failed summary does not commit a summary replacement.
|
|
39
|
+
- On overflow, retains the latest user message at the end when earlier user history is available, and compacts that earlier history first. After success, converts media in retained user messages to attachment descriptions and adds continuation instructions. Original attachments remain intact if summarization fails.
|
|
40
|
+
|
|
41
|
+
`reserveTokens`, `thresholdRatio`, and `keepRecentTokens` override the budgets; the ratio applies to the window after subtracting the reserve. `tailTurns` limits the maximum number of retained user turns; setting it to 0 summarizes all selected history. `maxSummaryTokens`, `summaryToolChars`, `summarizationProvider`, and `summarizationModel` control the auxiliary request. `maxOverflowRetries` defaults to 1 per request sequence. The optional `maxConsecutiveFailures` setting pauses automatic processing after repeated failures; a successful manual compaction resets the count.
|
|
42
|
+
|
|
43
|
+
## Implementation scope
|
|
44
|
+
|
|
45
|
+
Use `createPipeline(config)` directly through `run(host, trigger)` and `summarizeRange(host, entries)`. This package maintains the adapted algorithms, summary text, and state records. Previous summaries and recovery counters are recorded in the session log. `auto: false` disables DSH's automatic hooks; explicit calls still run.
|
|
46
|
+
|
|
47
|
+
OpenCode's provider-specific message encoders and third-party plugin hooks require its runtime. This package uses DSH's message representation and keeps tool calls and results together where the two representations group messages differently. Media continuation uses DSH user-message replacement records; original attachments remain in the historical log.
|
|
48
|
+
|
|
49
|
+
DSH's system and developer messages stay in place. The plugin applies OpenCode's retention algorithm to each history segment, skipping segments where only existing checkpoints would be selected. Usage accounting still covers the full context. Summaries merge every checkpoint in the current context and fall back to plugin state records only when the context has no checkpoints, avoiding stale summary metadata when restoring a session.
|
|
50
|
+
|
|
51
|
+
Original project: [anomalyco/opencode](https://github.com/anomalyco/opencode). The inspected local fork is pinned to `beb99270834db8eb62cf3a369e99234d4d4c2cbd`. The source project uses the MIT license. Budget and pruning behavior come from `packages/opencode/src/session/`; summary headings come from `packages/core/src/session/compaction.ts`, which that implementation calls. Summary instructions are defined in `src/strategy.ts`.
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# OpenCode 上下文策略
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 简体中文
|
|
4
|
+
|
|
5
|
+
本包实现 OpenCode 的用量判断、工具输出裁剪、保留区间选择、摘要请求、结果验证和溢出续接流程。DSH 负责读取日志、调用模型和提交替换。
|
|
6
|
+
|
|
7
|
+
## npm 安装
|
|
8
|
+
|
|
9
|
+
发布后可用 `dsh plugin --profile web add dsh-context-opencode` 安装。按 [npm 指南](https://github.com/zonemeen/dsh-context-zoo/blob/main/docs/publishing.zh-CN.md#使用已发布的包) 应用宿主 Session 补丁并生成启用配置。仅安装包不会替换当前上下文引擎。
|
|
10
|
+
|
|
11
|
+
## 上下文流程图
|
|
12
|
+
|
|
13
|
+
可选的工具输出裁剪先于自动触发阈值检查。近期回合保留在摘要替换范围外;溢出时若存在更早的用户历史,还会保留最新用户消息。对话转录文本的截短只影响摘要输入,原始消息仍保存在会话日志中。
|
|
14
|
+
|
|
15
|
+
```mermaid
|
|
16
|
+
flowchart TD
|
|
17
|
+
A["自动检查、手动压缩或溢出恢复"] --> B{"未超过连续失败与恢复次数限制?"}
|
|
18
|
+
B -->|否| L["保留当前上下文"]
|
|
19
|
+
B -->|是| C["可选工具输出裁剪<br/>默认关闭"]
|
|
20
|
+
C --> D{"达到自动阈值<br/>或由手动、溢出触发?"}
|
|
21
|
+
D -->|否| E["保留当前上下文<br/>已完成的裁剪仍然生效"]
|
|
22
|
+
D -->|是| F["选择较早历史<br/>保留近期回合和完整工具配对"]
|
|
23
|
+
F --> G{"有可汇总的历史?"}
|
|
24
|
+
G -->|否| E
|
|
25
|
+
G -->|是| H["汇总选中历史<br/>合并先前检查点"]
|
|
26
|
+
H --> I{"摘要与替换通过校验?"}
|
|
27
|
+
I -->|是| J["提交检查点并保留近期尾部<br/>溢出时将保留的媒体转为描述"]
|
|
28
|
+
I -->|否| K["不以摘要替换历史<br/>已完成的裁剪仍然生效"]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 默认行为
|
|
32
|
+
|
|
33
|
+
- 有 provider usage 时使用总用量,或累加输入、输出及缓存用量;无 usage 时,对去除 DSH 身份字段后的消息 JSON 使用字符数除以 4 的估算。
|
|
34
|
+
- 模型提供独立输入上限时,从该上限扣除 `reserveTokens`,缺省预留为输出预算与 20,000 的较小值。只有总窗口时扣除输出预算;输出预算缺失时使用 32,000。
|
|
35
|
+
- 用量达到阈值时触发。保留预算为可用窗口的 25%,最少 2,000、最多 15,000 tokens。先保留完整近期用户回合;一个回合放不下时寻找能容纳的后缀,并保持工具调用与结果完整。
|
|
36
|
+
- 工具输出裁剪默认关闭。开启后向前扫描,经过第二个用户消息才处理旧结果;保护累计前 40,000 tokens 的工具输出及 `skill`。遇到上次摘要或已清理结果时停止。候选输出严格超过 20,000 tokens 才清理,错误结果保留。
|
|
37
|
+
- 摘要请求使用单独序列化的对话文本,保留工具名和参数,每段工具输出默认最多 2,000 字符。前次摘要参与合并;章节为 Objective、Important Details、Work State、Next Move、Relevant Files。
|
|
38
|
+
- 摘要输出上限默认取当前输出预算与 32,000 的较小值。空白、截断、错误、工具调用及媒体响应被拒绝;失败不会提交摘要替换。
|
|
39
|
+
- 溢出时若存在更早的用户历史,将最新用户消息保留在尾部,先压缩之前的历史;成功后把保留用户消息中的媒体转换为附件描述,并附带续接指令。摘要失败时保留原始附件。
|
|
40
|
+
|
|
41
|
+
`reserveTokens`、`thresholdRatio`、`keepRecentTokens` 可以覆盖预算;比例作用于扣除预留后的窗口。`tailTurns` 限定最多保留的用户回合数,设为 0 时汇总全部选中历史。`maxSummaryTokens`、`summaryToolChars`、`summarizationProvider` 与 `summarizationModel` 控制辅助请求。`maxOverflowRetries` 默认每个请求序列 1 次;`maxConsecutiveFailures` 是可选的失败暂停配置,手动压缩成功后清零。
|
|
42
|
+
|
|
43
|
+
## 实现范围
|
|
44
|
+
|
|
45
|
+
可直接使用 `createPipeline(config)` 的 `run(host, trigger)` 和 `summarizeRange(host, entries)`。来源算法、摘要文本和状态记录由本包维护;前次摘要及恢复计数记录在会话日志中。`auto: false` 关闭 DSH 自动钩子,显式调用仍可执行。
|
|
46
|
+
|
|
47
|
+
OpenCode 的 provider 专用消息编码器和第三方插件钩子依赖其运行时。本包使用 DSH 的消息表示;在两种消息表示的分组不同处,优先保持工具调用与结果完整。媒体续接使用 DSH 用户消息替换记录,原始附件仍留在历史日志中。
|
|
48
|
+
|
|
49
|
+
DSH 的 system 与 developer 消息保留原位;插件逐段应用 OpenCode 的保留算法,跳过只能选中既有 checkpoint 的段。用量判断仍覆盖完整上下文。摘要合并当前上下文中的全部 checkpoint;只有当前上下文没有 checkpoint 时才回退到插件状态记录,避免恢复会话时使用落后的摘要元数据。
|
|
50
|
+
|
|
51
|
+
源项目为 [anomalyco/opencode](https://github.com/anomalyco/opencode);实际参考本地 fork 的提交 `beb99270834db8eb62cf3a369e99234d4d4c2cbd`,源项目采用 MIT 许可证。预算和裁剪行为对应 `packages/opencode/src/session/`;摘要章节来自该实现调用的 `packages/core/src/session/compaction.ts`。摘要指令定义在 `src/strategy.ts` 中。
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;;AAC/C,wBAA+E"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Context management plugin based on the OpenCode workflow. */
|
|
2
|
+
import { createContextPlugin } from 'dsh-context-core';
|
|
3
|
+
import { createPipeline } from './pipeline.js';
|
|
4
|
+
export { strategy } from './strategy.js';
|
|
5
|
+
export { createPipeline } from './pipeline.js';
|
|
6
|
+
export default createContextPlugin({ id: 'opencode', create: createPipeline });
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,eAAe,mBAAmB,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ContextConfig, ContextPipeline } from 'dsh-context-core';
|
|
2
|
+
/** Create a source-specific pipeline whose recovery state survives session replay. */
|
|
3
|
+
export declare function createPipeline(config?: ContextConfig): ContextPipeline;
|
|
4
|
+
//# sourceMappingURL=pipeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAc,aAAa,EAA6B,eAAe,EAAmB,MAAM,kBAAkB,CAAC;AAgI/H,sFAAsF;AACtF,wBAAgB,cAAc,CAAC,MAAM,GAAE,aAAkB,GAAG,eAAe,CAyE1E"}
|
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { strategy } from './strategy.js';
|
|
2
|
+
const CLEARED = '[Old tool result content cleared]';
|
|
3
|
+
const textOf = (entry) => entry.message.content.map(block => block.type === 'text' ? block.text : '').join('\n');
|
|
4
|
+
const checkpoint = (entry) => entry.message.source.kind === 'compact-checkpoint';
|
|
5
|
+
const conversational = (entry) => entry.message.role !== 'system' && entry.message.role !== 'developer';
|
|
6
|
+
/** Protected instructions split the surface into separately compactable spans. */
|
|
7
|
+
function spans(entries) {
|
|
8
|
+
const result = [];
|
|
9
|
+
let current = [];
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
if (conversational(entry))
|
|
12
|
+
current.push(entry);
|
|
13
|
+
else if (current.length) {
|
|
14
|
+
result.push(current);
|
|
15
|
+
current = [];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (current.length)
|
|
19
|
+
result.push(current);
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
/** Provider totals include cache reads, cache writes, and output tokens. */
|
|
23
|
+
function usageTokens(snapshot) {
|
|
24
|
+
const usage = snapshot.entries.findLast(entry => entry.message.role === 'assistant' && entry.usage !== undefined)?.usage;
|
|
25
|
+
return usage === undefined ? estimate(snapshot.entries) : usage.totalTokens || usage.inputTokens + usage.outputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
|
|
26
|
+
}
|
|
27
|
+
function estimate(entries) {
|
|
28
|
+
return Math.max(0, Math.round(JSON.stringify(entries.map(({ message }) => ({
|
|
29
|
+
role: message.role, content: message.content,
|
|
30
|
+
...message.role === 'tool' ? { toolCallId: message.toolCallId } : {},
|
|
31
|
+
}))).length / 4));
|
|
32
|
+
}
|
|
33
|
+
function usable(snapshot, config) {
|
|
34
|
+
const output = snapshot.maxOutputTokens || 32_000;
|
|
35
|
+
if (snapshot.inputLimit)
|
|
36
|
+
return Math.max(0, snapshot.inputLimit - (config.reserveTokens ?? Math.min(20_000, output)));
|
|
37
|
+
return Math.max(0, snapshot.contextWindow - (config.reserveTokens ?? output));
|
|
38
|
+
}
|
|
39
|
+
/** A suffix cannot separate a tool call from its results. */
|
|
40
|
+
function balancedCuts(entries) {
|
|
41
|
+
const cuts = new Set([0]);
|
|
42
|
+
const pending = new Set();
|
|
43
|
+
entries.forEach((entry, index) => {
|
|
44
|
+
for (const block of entry.message.content)
|
|
45
|
+
if (block.type === 'tool-call')
|
|
46
|
+
pending.add(block.id);
|
|
47
|
+
if (entry.message.role === 'tool')
|
|
48
|
+
pending.delete(entry.message.toolCallId);
|
|
49
|
+
if (pending.size === 0)
|
|
50
|
+
cuts.add(index + 1);
|
|
51
|
+
});
|
|
52
|
+
return cuts;
|
|
53
|
+
}
|
|
54
|
+
/** Retain complete turns, then the earliest fitting balanced suffix of one oversized turn. */
|
|
55
|
+
function select(entries, budget, tailTurns) {
|
|
56
|
+
if (tailTurns === 0)
|
|
57
|
+
return entries;
|
|
58
|
+
const turns = entries.flatMap((entry, index) => entry.message.role === 'user' && !checkpoint(entry) ? [index] : []);
|
|
59
|
+
if (!turns.length)
|
|
60
|
+
return entries;
|
|
61
|
+
const candidates = tailTurns === undefined ? turns : turns.slice(-tailTurns);
|
|
62
|
+
const cuts = balancedCuts(entries);
|
|
63
|
+
let kept = entries.length;
|
|
64
|
+
let total = 0;
|
|
65
|
+
for (let index = candidates.length - 1; index >= 0; index--) {
|
|
66
|
+
const start = candidates[index];
|
|
67
|
+
const end = turns[turns.indexOf(start) + 1] ?? entries.length;
|
|
68
|
+
const size = estimate(entries.slice(start, end));
|
|
69
|
+
if (total + size <= budget && cuts.has(start)) {
|
|
70
|
+
total += size;
|
|
71
|
+
kept = start;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (let suffix = start + 1; suffix < end; suffix++) {
|
|
75
|
+
if (!cuts.has(suffix) || entries[suffix].message.role === 'tool')
|
|
76
|
+
continue;
|
|
77
|
+
if (estimate(entries.slice(suffix, end)) <= budget - total) {
|
|
78
|
+
kept = suffix;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
return kept === 0 ? entries : entries.slice(0, kept);
|
|
85
|
+
}
|
|
86
|
+
function mediaText(block) {
|
|
87
|
+
if (block.type === 'image')
|
|
88
|
+
return { type: 'text', text: `[Attached ${block.attachment.mediaType}: image]` };
|
|
89
|
+
if (block.type === 'file')
|
|
90
|
+
return { type: 'text', text: '[Attached file]' };
|
|
91
|
+
return block;
|
|
92
|
+
}
|
|
93
|
+
function serialize(entries, limit) {
|
|
94
|
+
return entries.filter(entry => !checkpoint(entry)).map(entry => {
|
|
95
|
+
const message = entry.message;
|
|
96
|
+
const blocks = message.content.map(mediaText);
|
|
97
|
+
if (message.role === 'tool') {
|
|
98
|
+
const text = blocks.map(block => block.type === 'text' ? block.text : '').join('\n');
|
|
99
|
+
const output = limit > 0 && text.length > limit ? `${text.slice(0, limit)}\n[truncated]` : text;
|
|
100
|
+
return `[${message.isError ? 'Tool error' : 'Tool result'}]: ${output}`;
|
|
101
|
+
}
|
|
102
|
+
return blocks.map(block => {
|
|
103
|
+
if (block.type === 'tool-call')
|
|
104
|
+
return `[Assistant tool call]: ${block.name}(${block.arguments})`;
|
|
105
|
+
if (block.type === 'reasoning')
|
|
106
|
+
return `[Assistant reasoning]: ${block.text}`;
|
|
107
|
+
return block.type === 'text' ? `[${message.role === 'user' ? 'User' : 'Assistant'}]: ${block.text}` : '';
|
|
108
|
+
}).filter(Boolean).join('\n');
|
|
109
|
+
}).join('\n\n');
|
|
110
|
+
}
|
|
111
|
+
function prune(host, snapshot, config) {
|
|
112
|
+
if (config.prune !== true)
|
|
113
|
+
return;
|
|
114
|
+
const names = new Map();
|
|
115
|
+
for (const entry of snapshot.entries)
|
|
116
|
+
for (const block of entry.message.content)
|
|
117
|
+
if (block.type === 'tool-call')
|
|
118
|
+
names.set(block.id, block.name);
|
|
119
|
+
let turns = 0;
|
|
120
|
+
let total = 0;
|
|
121
|
+
let savings = 0;
|
|
122
|
+
let keptTools = 0;
|
|
123
|
+
const selected = [];
|
|
124
|
+
for (const entry of [...snapshot.entries].reverse()) {
|
|
125
|
+
if (checkpoint(entry))
|
|
126
|
+
break;
|
|
127
|
+
if (entry.message.role === 'user')
|
|
128
|
+
turns++;
|
|
129
|
+
if (turns < 2 || entry.message.role !== 'tool' || entry.message.isError)
|
|
130
|
+
continue;
|
|
131
|
+
if ((entry.toolName ?? names.get(entry.message.toolCallId)) === 'skill')
|
|
132
|
+
continue;
|
|
133
|
+
const output = textOf(entry);
|
|
134
|
+
if (output === CLEARED)
|
|
135
|
+
break;
|
|
136
|
+
const price = Math.max(0, Math.round(output.length / 4));
|
|
137
|
+
total += price;
|
|
138
|
+
if (keptTools++ < (config.keepRecentTools ?? 0) || total <= (config.protectToolTokens ?? 40_000))
|
|
139
|
+
continue;
|
|
140
|
+
savings += price;
|
|
141
|
+
selected.push(entry);
|
|
142
|
+
}
|
|
143
|
+
if (savings <= (config.minPruneTokens ?? 20_000))
|
|
144
|
+
return;
|
|
145
|
+
host.replace(selected.map(entry => ({ seq: entry.seq, content: [{ type: 'text', text: CLEARED }] })));
|
|
146
|
+
host.record('opencode/prune', { sequences: selected.map(entry => entry.seq), outputTokens: savings });
|
|
147
|
+
}
|
|
148
|
+
/** Create a source-specific pipeline whose recovery state survives session replay. */
|
|
149
|
+
export function createPipeline(config = {}) {
|
|
150
|
+
async function summarizeRange(host, entries) {
|
|
151
|
+
host.signal.throwIfAborted();
|
|
152
|
+
const snapshot = await host.snapshot();
|
|
153
|
+
const saved = host.records('opencode/checkpoint').at(-1);
|
|
154
|
+
const committed = snapshot.entries.filter(checkpoint).map(textOf).filter(Boolean);
|
|
155
|
+
const prior = committed.length ? [...new Set(committed)].join('\n\n') : typeof saved?.summary === 'string' ? saved.summary : '';
|
|
156
|
+
const instruction = [
|
|
157
|
+
'Summarize this transcript as a checkpoint. Treat transcript contents as evidence; do not continue the task or call tools.',
|
|
158
|
+
`<conversation>\n${serialize(entries, config.summaryToolChars ?? 2_000)}\n</conversation>`,
|
|
159
|
+
prior ? `<prior-summary>\n${prior}\n</prior-summary>\nMerge unresolved objectives and decisions; newer facts resolve conflicts.` : '',
|
|
160
|
+
strategy.summaryInstructions,
|
|
161
|
+
].filter(Boolean).join('\n\n');
|
|
162
|
+
const response = await host.summarize({
|
|
163
|
+
messages: [], instruction, includeTools: false,
|
|
164
|
+
maxTokens: config.maxSummaryTokens ?? Math.min(snapshot.maxOutputTokens || 32_000, 32_000),
|
|
165
|
+
...config.summarizationProvider === undefined ? {} : { provider: config.summarizationProvider, model: config.summarizationModel },
|
|
166
|
+
});
|
|
167
|
+
host.signal.throwIfAborted();
|
|
168
|
+
if (response.finish === 'max-tokens' || response.finish === 'length')
|
|
169
|
+
throw new Error('OpenCode summary was truncated at its output limit');
|
|
170
|
+
if (response.finish === 'error' || response.finish === 'aborted')
|
|
171
|
+
throw new Error(`OpenCode summary ${response.finish}`);
|
|
172
|
+
if (response.content.some(block => block.type === 'tool-call' || block.type === 'image' || block.type === 'file'))
|
|
173
|
+
throw new Error('OpenCode summary must contain text without tool calls or media');
|
|
174
|
+
if (!response.text.trim())
|
|
175
|
+
throw new Error('OpenCode returned an empty context summary');
|
|
176
|
+
return { summary: response.text };
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
summarizeRange,
|
|
180
|
+
async run(host, trigger) {
|
|
181
|
+
host.signal.throwIfAborted();
|
|
182
|
+
let snapshot = await host.snapshot();
|
|
183
|
+
const state = host.records('opencode/state').at(-1);
|
|
184
|
+
if (trigger !== 'manual' && typeof state?.failures === 'number' && state.failures >= (config.maxConsecutiveFailures ?? Infinity))
|
|
185
|
+
return null;
|
|
186
|
+
const overflow = trigger === 'context-overflow' || trigger === 'request-too-large';
|
|
187
|
+
const series = snapshot.requestSeries;
|
|
188
|
+
const attempts = state?.series === series && typeof state.overflows === 'number' ? state.overflows : 0;
|
|
189
|
+
if (overflow && attempts >= (config.maxOverflowRetries ?? 1))
|
|
190
|
+
return null;
|
|
191
|
+
prune(host, snapshot, config);
|
|
192
|
+
snapshot = await host.snapshot();
|
|
193
|
+
if (trigger === 'pressure' && (snapshot.contextWindow <= 0 || usageTokens(snapshot) < Math.floor(usable(snapshot, config) * (config.thresholdRatio ?? 1))))
|
|
194
|
+
return null;
|
|
195
|
+
const budget = config.keepRecentTokens ?? Math.min(15_000, Math.max(2_000, Math.floor(usable(snapshot, config) * 0.25)));
|
|
196
|
+
let selected;
|
|
197
|
+
const latestUser = snapshot.entries.findLastIndex(entry => entry.message.role === 'user' && !checkpoint(entry));
|
|
198
|
+
const history = overflow && latestUser > 0 && snapshot.entries.slice(0, latestUser).some(entry => entry.message.role === 'user' && !checkpoint(entry))
|
|
199
|
+
? snapshot.entries.slice(0, latestUser) : snapshot.entries;
|
|
200
|
+
for (const span of spans(history)) {
|
|
201
|
+
const candidate = select(span, budget, config.tailTurns);
|
|
202
|
+
if (candidate.length && candidate.some(entry => !checkpoint(entry))) {
|
|
203
|
+
selected = candidate;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!selected)
|
|
208
|
+
return null;
|
|
209
|
+
const range = selected;
|
|
210
|
+
let generated;
|
|
211
|
+
try {
|
|
212
|
+
const result = await host.compact(range, async () => {
|
|
213
|
+
generated = await summarizeRange(host, range);
|
|
214
|
+
if (trigger !== 'manual')
|
|
215
|
+
generated.restored = [{ type: 'text', text: overflow
|
|
216
|
+
? 'The request exceeded the provider context limit. Continue the latest request using the checkpoint and retained messages; attachments are represented by descriptions.'
|
|
217
|
+
: 'Continue the unfinished work when the next step is clear; otherwise ask for the missing information.' }];
|
|
218
|
+
return generated;
|
|
219
|
+
});
|
|
220
|
+
if (overflow) {
|
|
221
|
+
const live = await host.snapshot();
|
|
222
|
+
host.replace(live.entries.filter(entry => entry.message.role === 'user' && entry.message.content.some(block => block.type === 'image' || block.type === 'file')).map(entry => ({ seq: entry.seq, content: entry.message.content.map(mediaText) })));
|
|
223
|
+
}
|
|
224
|
+
host.record('opencode/checkpoint', { summary: generated.summary, time: snapshot.now, selected: range.map(entry => entry.seq) });
|
|
225
|
+
host.record('opencode/state', { failures: 0, series, overflows: overflow ? attempts + 1 : 0 });
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (!host.signal.aborted)
|
|
230
|
+
host.record('opencode/state', { failures: (typeof state?.failures === 'number' ? state.failures : 0) + 1, series, overflows: overflow ? attempts + 1 : attempts });
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** OpenCode context-budget, recent-history, and optional output-pruning strategy. */
|
|
2
|
+
import type { ContextStrategy } from 'dsh-context-core';
|
|
3
|
+
/** Adaptation of the pinned OpenCode session compaction implementation. */
|
|
4
|
+
export declare const strategy: ContextStrategy;
|
|
5
|
+
//# sourceMappingURL=strategy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategy.d.ts","sourceRoot":"","sources":["../src/strategy.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,2EAA2E;AAC3E,eAAO,MAAM,QAAQ,EAAE,eA2CtB,CAAC"}
|
package/dist/strategy.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Adaptation of the pinned OpenCode session compaction implementation. */
|
|
2
|
+
export const strategy = {
|
|
3
|
+
id: 'opencode',
|
|
4
|
+
name: 'OpenCode',
|
|
5
|
+
source: {
|
|
6
|
+
url: 'https://github.com/anomalyco/opencode',
|
|
7
|
+
revision: 'beb99270834db8eb62cf3a369e99234d4d4c2cbd',
|
|
8
|
+
license: 'MIT',
|
|
9
|
+
},
|
|
10
|
+
limitations: [
|
|
11
|
+
'DSH message blocks are serialized by this package; provider-specific OpenCode wire encoders and third-party OpenCode hooks require their native runtime.',
|
|
12
|
+
'Tool-call pairs remain complete when a DSH message boundary differs from an OpenCode assistant part.',
|
|
13
|
+
],
|
|
14
|
+
budget(input) {
|
|
15
|
+
const reserve = input.reserveTokens ?? (input.maxOutputTokens || 32_000);
|
|
16
|
+
const usable = Math.max(0, input.contextWindow - reserve);
|
|
17
|
+
return {
|
|
18
|
+
triggerTokens: Math.floor(usable * (input.thresholdRatio ?? 1)),
|
|
19
|
+
retainTokens: input.keepRecentTokens ?? Math.min(15_000, Math.max(2_000, Math.floor(usable * 0.25))),
|
|
20
|
+
tailMode: 'maximum',
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
summaryInstructions: `Write a concise handoff using these headings in order:
|
|
24
|
+
## Objective
|
|
25
|
+
## Important Details
|
|
26
|
+
## Work State
|
|
27
|
+
### Completed
|
|
28
|
+
### Active
|
|
29
|
+
### Blocked
|
|
30
|
+
## Next Move
|
|
31
|
+
## Relevant Files
|
|
32
|
+
|
|
33
|
+
Record the user's goal, constraints, decisions, completed work, pending work, blockers, and next actions. Include exact paths, symbols, commands, and errors needed to continue. Integrate any prior summary; resolve conflicting facts using the newer conversation. Keep unfinished requests and relevant earlier decisions. Use short bullets and keep empty sections with "(none)".`,
|
|
34
|
+
maxSummaryTokens: 32_000,
|
|
35
|
+
summaryToolChars: 2_000,
|
|
36
|
+
prune: {
|
|
37
|
+
enabled: false,
|
|
38
|
+
keepRecentTools: 0,
|
|
39
|
+
keepRecentTurns: 2,
|
|
40
|
+
protectTokens: 40_000,
|
|
41
|
+
minSavingsTokens: 20_000,
|
|
42
|
+
strictSavings: true,
|
|
43
|
+
protectedTools: ['skill'],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=strategy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"strategy.js","sourceRoot":"","sources":["../src/strategy.ts"],"names":[],"mappings":"AAGA,2EAA2E;AAC3E,MAAM,CAAC,MAAM,QAAQ,GAAoB;IACvC,EAAE,EAAE,UAAU;IACd,IAAI,EAAE,UAAU;IAChB,MAAM,EAAE;QACN,GAAG,EAAE,uCAAuC;QAC5C,QAAQ,EAAE,0CAA0C;QACpD,OAAO,EAAE,KAAK;KACf;IACD,WAAW,EAAE;QACX,0JAA0J;QAC1J,sGAAsG;KACvG;IACD,MAAM,CAAC,KAAK;QACV,MAAM,OAAO,GAAG,KAAK,CAAC,aAAa,IAAI,CAAC,KAAK,CAAC,eAAe,IAAI,MAAM,CAAC,CAAC;QACzE,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,aAAa,GAAG,OAAO,CAAC,CAAC;QAC1D,OAAO;YACL,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,cAAc,IAAI,CAAC,CAAC,CAAC;YAC/D,YAAY,EAAE,KAAK,CAAC,gBAAgB,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;YACpG,QAAQ,EAAE,SAAS;SACpB,CAAC;IACJ,CAAC;IACD,mBAAmB,EAAE;;;;;;;;;;wXAUiW;IACtX,gBAAgB,EAAE,MAAM;IACxB,gBAAgB,EAAE,KAAK;IACvB,KAAK,EAAE;QACL,OAAO,EAAE,KAAK;QACd,eAAe,EAAE,CAAC;QAClB,eAAe,EAAE,CAAC;QAClB,aAAa,EAAE,MAAM;QACrB,gBAAgB,EAAE,MAAM;QACxB,aAAa,EAAE,IAAI;QACnB,cAAc,EAAE,CAAC,OAAO,CAAC;KAC1B;CACF,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-context-opencode",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "opencode context management strategy adapted for DeepSeek Harness.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/zonemeen/dsh-context-zoo.git",
|
|
10
|
+
"directory": "packages/opencode"
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist/**/*.js",
|
|
23
|
+
"dist/**/*.js.map",
|
|
24
|
+
"dist/**/*.d.ts",
|
|
25
|
+
"dist/**/*.d.ts.map",
|
|
26
|
+
"README.md",
|
|
27
|
+
"README.zh-CN.md",
|
|
28
|
+
"src",
|
|
29
|
+
"LICENSE"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"registry": "https://registry.npmjs.org/"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"dsh-context-core": "^0.1.1"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@deepseek-ai/dsh-agent": "0.1.7-rc.2",
|
|
43
|
+
"@deepseek-ai/dsh-commands": "0.1.7-rc.2",
|
|
44
|
+
"@deepseek-ai/dsh-compaction": "0.1.7-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-llm": "0.1.7-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-session": "0.1.7-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-token-meter": "0.1.7-rc.2",
|
|
48
|
+
"@deepseek-ai/cordis": "~4.0.4",
|
|
49
|
+
"@deepseek-ai/dsh-fs": "0.1.7-rc.2"
|
|
50
|
+
},
|
|
51
|
+
"dsh": {
|
|
52
|
+
"bundle": {
|
|
53
|
+
"patch": []
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/zonemeen/dsh-context-zoo/tree/main/packages/opencode#readme",
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/zonemeen/dsh-context-zoo/issues"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Context management plugin based on the OpenCode workflow. */
|
|
2
|
+
import { createContextPlugin } from 'dsh-context-core';
|
|
3
|
+
import { createPipeline } from './pipeline.js';
|
|
4
|
+
export { strategy } from './strategy.js';
|
|
5
|
+
export { createPipeline } from './pipeline.js';
|
|
6
|
+
export default createContextPlugin({ id: 'opencode', create: createPipeline });
|
package/src/pipeline.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/** Usage accounting, pruning, tail selection, and continuation based on the OpenCode workflow. */
|
|
2
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm';
|
|
3
|
+
import type { Checkpoint, ContextConfig, ContextEntry, ContextHost, ContextPipeline, ContextSnapshot } from 'dsh-context-core';
|
|
4
|
+
import { strategy } from './strategy.js';
|
|
5
|
+
|
|
6
|
+
const CLEARED = '[Old tool result content cleared]';
|
|
7
|
+
const textOf = (entry: ContextEntry): string => entry.message.content.map(block => block.type === 'text' ? block.text : '').join('\n');
|
|
8
|
+
const checkpoint = (entry: ContextEntry): boolean => entry.message.source.kind === 'compact-checkpoint';
|
|
9
|
+
const conversational = (entry: ContextEntry): boolean => entry.message.role !== 'system' && entry.message.role !== 'developer';
|
|
10
|
+
|
|
11
|
+
/** Protected instructions split the surface into separately compactable spans. */
|
|
12
|
+
function spans(entries: readonly ContextEntry[]): ContextEntry[][] {
|
|
13
|
+
const result: ContextEntry[][] = [];
|
|
14
|
+
let current: ContextEntry[] = [];
|
|
15
|
+
for (const entry of entries) {
|
|
16
|
+
if (conversational(entry)) current.push(entry);
|
|
17
|
+
else if (current.length) { result.push(current); current = []; }
|
|
18
|
+
}
|
|
19
|
+
if (current.length) result.push(current);
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Provider totals include cache reads, cache writes, and output tokens. */
|
|
24
|
+
function usageTokens(snapshot: ContextSnapshot): number {
|
|
25
|
+
const usage = snapshot.entries.findLast(entry => entry.message.role === 'assistant' && entry.usage !== undefined)?.usage;
|
|
26
|
+
return usage === undefined ? estimate(snapshot.entries) : usage.totalTokens || usage.inputTokens + usage.outputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function estimate(entries: readonly ContextEntry[]): number {
|
|
30
|
+
return Math.max(0, Math.round(JSON.stringify(entries.map(({ message }) => ({
|
|
31
|
+
role: message.role, content: message.content,
|
|
32
|
+
...message.role === 'tool' ? { toolCallId: message.toolCallId } : {},
|
|
33
|
+
}))).length / 4));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function usable(snapshot: ContextSnapshot, config: ContextConfig): number {
|
|
37
|
+
const output = snapshot.maxOutputTokens || 32_000;
|
|
38
|
+
if (snapshot.inputLimit) return Math.max(0, snapshot.inputLimit - (config.reserveTokens ?? Math.min(20_000, output)));
|
|
39
|
+
return Math.max(0, snapshot.contextWindow - (config.reserveTokens ?? output));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** A suffix cannot separate a tool call from its results. */
|
|
43
|
+
function balancedCuts(entries: readonly ContextEntry[]): Set<number> {
|
|
44
|
+
const cuts = new Set([0]);
|
|
45
|
+
const pending = new Set<string>();
|
|
46
|
+
entries.forEach((entry, index) => {
|
|
47
|
+
for (const block of entry.message.content) if (block.type === 'tool-call') pending.add(block.id);
|
|
48
|
+
if (entry.message.role === 'tool') pending.delete(entry.message.toolCallId);
|
|
49
|
+
if (pending.size === 0) cuts.add(index + 1);
|
|
50
|
+
});
|
|
51
|
+
return cuts;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Retain complete turns, then the earliest fitting balanced suffix of one oversized turn. */
|
|
55
|
+
function select(entries: readonly ContextEntry[], budget: number, tailTurns?: number): readonly ContextEntry[] {
|
|
56
|
+
if (tailTurns === 0) return entries;
|
|
57
|
+
const turns = entries.flatMap((entry, index) => entry.message.role === 'user' && !checkpoint(entry) ? [index] : []);
|
|
58
|
+
if (!turns.length) return entries;
|
|
59
|
+
const candidates = tailTurns === undefined ? turns : turns.slice(-tailTurns);
|
|
60
|
+
const cuts = balancedCuts(entries);
|
|
61
|
+
let kept = entries.length;
|
|
62
|
+
let total = 0;
|
|
63
|
+
for (let index = candidates.length - 1; index >= 0; index--) {
|
|
64
|
+
const start = candidates[index]!;
|
|
65
|
+
const end = turns[turns.indexOf(start) + 1] ?? entries.length;
|
|
66
|
+
const size = estimate(entries.slice(start, end));
|
|
67
|
+
if (total + size <= budget && cuts.has(start)) {
|
|
68
|
+
total += size;
|
|
69
|
+
kept = start;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
for (let suffix = start + 1; suffix < end; suffix++) {
|
|
73
|
+
if (!cuts.has(suffix) || entries[suffix]!.message.role === 'tool') continue;
|
|
74
|
+
if (estimate(entries.slice(suffix, end)) <= budget - total) { kept = suffix; break; }
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
return kept === 0 ? entries : entries.slice(0, kept);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function mediaText(block: ContentBlock): ContentBlock {
|
|
82
|
+
if (block.type === 'image') return { type: 'text', text: `[Attached ${block.attachment.mediaType}: image]` };
|
|
83
|
+
if (block.type === 'file') return { type: 'text', text: '[Attached file]' };
|
|
84
|
+
return block;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function serialize(entries: readonly ContextEntry[], limit: number): string {
|
|
88
|
+
return entries.filter(entry => !checkpoint(entry)).map(entry => {
|
|
89
|
+
const message = entry.message;
|
|
90
|
+
const blocks = message.content.map(mediaText);
|
|
91
|
+
if (message.role === 'tool') {
|
|
92
|
+
const text = blocks.map(block => block.type === 'text' ? block.text : '').join('\n');
|
|
93
|
+
const output = limit > 0 && text.length > limit ? `${text.slice(0, limit)}\n[truncated]` : text;
|
|
94
|
+
return `[${message.isError ? 'Tool error' : 'Tool result'}]: ${output}`;
|
|
95
|
+
}
|
|
96
|
+
return blocks.map(block => {
|
|
97
|
+
if (block.type === 'tool-call') return `[Assistant tool call]: ${block.name}(${block.arguments})`;
|
|
98
|
+
if (block.type === 'reasoning') return `[Assistant reasoning]: ${block.text}`;
|
|
99
|
+
return block.type === 'text' ? `[${message.role === 'user' ? 'User' : 'Assistant'}]: ${block.text}` : '';
|
|
100
|
+
}).filter(Boolean).join('\n');
|
|
101
|
+
}).join('\n\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function prune(host: ContextHost, snapshot: ContextSnapshot, config: ContextConfig): void {
|
|
105
|
+
if (config.prune !== true) return;
|
|
106
|
+
const names = new Map<string, string>();
|
|
107
|
+
for (const entry of snapshot.entries) for (const block of entry.message.content) if (block.type === 'tool-call') names.set(block.id, block.name);
|
|
108
|
+
let turns = 0;
|
|
109
|
+
let total = 0;
|
|
110
|
+
let savings = 0;
|
|
111
|
+
let keptTools = 0;
|
|
112
|
+
const selected: ContextEntry[] = [];
|
|
113
|
+
for (const entry of [...snapshot.entries].reverse()) {
|
|
114
|
+
if (checkpoint(entry)) break;
|
|
115
|
+
if (entry.message.role === 'user') turns++;
|
|
116
|
+
if (turns < 2 || entry.message.role !== 'tool' || entry.message.isError) continue;
|
|
117
|
+
if ((entry.toolName ?? names.get(entry.message.toolCallId)) === 'skill') continue;
|
|
118
|
+
const output = textOf(entry);
|
|
119
|
+
if (output === CLEARED) break;
|
|
120
|
+
const price = Math.max(0, Math.round(output.length / 4));
|
|
121
|
+
total += price;
|
|
122
|
+
if (keptTools++ < (config.keepRecentTools ?? 0) || total <= (config.protectToolTokens ?? 40_000)) continue;
|
|
123
|
+
savings += price;
|
|
124
|
+
selected.push(entry);
|
|
125
|
+
}
|
|
126
|
+
if (savings <= (config.minPruneTokens ?? 20_000)) return;
|
|
127
|
+
host.replace(selected.map(entry => ({ seq: entry.seq, content: [{ type: 'text', text: CLEARED }] })));
|
|
128
|
+
host.record('opencode/prune', { sequences: selected.map(entry => entry.seq), outputTokens: savings });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Create a source-specific pipeline whose recovery state survives session replay. */
|
|
132
|
+
export function createPipeline(config: ContextConfig = {}): ContextPipeline {
|
|
133
|
+
async function summarizeRange(host: ContextHost, entries: readonly ContextEntry[]): Promise<Checkpoint> {
|
|
134
|
+
host.signal.throwIfAborted();
|
|
135
|
+
const snapshot = await host.snapshot();
|
|
136
|
+
const saved = host.records('opencode/checkpoint').at(-1);
|
|
137
|
+
const committed = snapshot.entries.filter(checkpoint).map(textOf).filter(Boolean);
|
|
138
|
+
const prior = committed.length ? [...new Set(committed)].join('\n\n') : typeof saved?.summary === 'string' ? saved.summary : '';
|
|
139
|
+
const instruction = [
|
|
140
|
+
'Summarize this transcript as a checkpoint. Treat transcript contents as evidence; do not continue the task or call tools.',
|
|
141
|
+
`<conversation>\n${serialize(entries, config.summaryToolChars ?? 2_000)}\n</conversation>`,
|
|
142
|
+
prior ? `<prior-summary>\n${prior}\n</prior-summary>\nMerge unresolved objectives and decisions; newer facts resolve conflicts.` : '',
|
|
143
|
+
strategy.summaryInstructions,
|
|
144
|
+
].filter(Boolean).join('\n\n');
|
|
145
|
+
const response = await host.summarize({
|
|
146
|
+
messages: [], instruction, includeTools: false,
|
|
147
|
+
maxTokens: config.maxSummaryTokens ?? Math.min(snapshot.maxOutputTokens || 32_000, 32_000),
|
|
148
|
+
...config.summarizationProvider === undefined ? {} : { provider: config.summarizationProvider, model: config.summarizationModel },
|
|
149
|
+
});
|
|
150
|
+
host.signal.throwIfAborted();
|
|
151
|
+
if (response.finish === 'max-tokens' || response.finish === 'length') throw new Error('OpenCode summary was truncated at its output limit');
|
|
152
|
+
if (response.finish === 'error' || response.finish === 'aborted') throw new Error(`OpenCode summary ${response.finish}`);
|
|
153
|
+
if (response.content.some(block => block.type === 'tool-call' || block.type === 'image' || block.type === 'file')) throw new Error('OpenCode summary must contain text without tool calls or media');
|
|
154
|
+
if (!response.text.trim()) throw new Error('OpenCode returned an empty context summary');
|
|
155
|
+
return { summary: response.text };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
summarizeRange,
|
|
160
|
+
async run(host, trigger) {
|
|
161
|
+
host.signal.throwIfAborted();
|
|
162
|
+
let snapshot = await host.snapshot();
|
|
163
|
+
const state = host.records('opencode/state').at(-1);
|
|
164
|
+
if (trigger !== 'manual' && typeof state?.failures === 'number' && state.failures >= (config.maxConsecutiveFailures ?? Infinity)) return null;
|
|
165
|
+
const overflow = trigger === 'context-overflow' || trigger === 'request-too-large';
|
|
166
|
+
const series = snapshot.requestSeries;
|
|
167
|
+
const attempts = state?.series === series && typeof state.overflows === 'number' ? state.overflows : 0;
|
|
168
|
+
if (overflow && attempts >= (config.maxOverflowRetries ?? 1)) return null;
|
|
169
|
+
prune(host, snapshot, config);
|
|
170
|
+
snapshot = await host.snapshot();
|
|
171
|
+
if (trigger === 'pressure' && (snapshot.contextWindow <= 0 || usageTokens(snapshot) < Math.floor(usable(snapshot, config) * (config.thresholdRatio ?? 1)))) return null;
|
|
172
|
+
const budget = config.keepRecentTokens ?? Math.min(15_000, Math.max(2_000, Math.floor(usable(snapshot, config) * 0.25)));
|
|
173
|
+
let selected: readonly ContextEntry[] | undefined;
|
|
174
|
+
const latestUser = snapshot.entries.findLastIndex(entry => entry.message.role === 'user' && !checkpoint(entry));
|
|
175
|
+
const history = overflow && latestUser > 0 && snapshot.entries.slice(0, latestUser).some(entry => entry.message.role === 'user' && !checkpoint(entry))
|
|
176
|
+
? snapshot.entries.slice(0, latestUser) : snapshot.entries;
|
|
177
|
+
for (const span of spans(history)) {
|
|
178
|
+
const candidate = select(span, budget, config.tailTurns);
|
|
179
|
+
if (candidate.length && candidate.some(entry => !checkpoint(entry))) { selected = candidate; break; }
|
|
180
|
+
}
|
|
181
|
+
if (!selected) return null;
|
|
182
|
+
const range = selected;
|
|
183
|
+
let generated: Checkpoint | undefined;
|
|
184
|
+
try {
|
|
185
|
+
const result = await host.compact(range, async () => {
|
|
186
|
+
generated = await summarizeRange(host, range);
|
|
187
|
+
if (trigger !== 'manual') generated.restored = [{ type: 'text', text: overflow
|
|
188
|
+
? 'The request exceeded the provider context limit. Continue the latest request using the checkpoint and retained messages; attachments are represented by descriptions.'
|
|
189
|
+
: 'Continue the unfinished work when the next step is clear; otherwise ask for the missing information.' }];
|
|
190
|
+
return generated;
|
|
191
|
+
});
|
|
192
|
+
if (overflow) {
|
|
193
|
+
const live = await host.snapshot();
|
|
194
|
+
host.replace(live.entries.filter(entry => entry.message.role === 'user' && entry.message.content.some(block => block.type === 'image' || block.type === 'file')).map(entry => ({ seq: entry.seq, content: entry.message.content.map(mediaText) })));
|
|
195
|
+
}
|
|
196
|
+
host.record('opencode/checkpoint', { summary: generated!.summary, time: snapshot.now, selected: range.map(entry => entry.seq) });
|
|
197
|
+
host.record('opencode/state', { failures: 0, series, overflows: overflow ? attempts + 1 : 0 });
|
|
198
|
+
return result;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (!host.signal.aborted) host.record('opencode/state', { failures: (typeof state?.failures === 'number' ? state.failures : 0) + 1, series, overflows: overflow ? attempts + 1 : attempts });
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
package/src/strategy.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** OpenCode context-budget, recent-history, and optional output-pruning strategy. */
|
|
2
|
+
import type { ContextStrategy } from 'dsh-context-core';
|
|
3
|
+
|
|
4
|
+
/** Adaptation of the pinned OpenCode session compaction implementation. */
|
|
5
|
+
export const strategy: ContextStrategy = {
|
|
6
|
+
id: 'opencode',
|
|
7
|
+
name: 'OpenCode',
|
|
8
|
+
source: {
|
|
9
|
+
url: 'https://github.com/anomalyco/opencode',
|
|
10
|
+
revision: 'beb99270834db8eb62cf3a369e99234d4d4c2cbd',
|
|
11
|
+
license: 'MIT',
|
|
12
|
+
},
|
|
13
|
+
limitations: [
|
|
14
|
+
'DSH message blocks are serialized by this package; provider-specific OpenCode wire encoders and third-party OpenCode hooks require their native runtime.',
|
|
15
|
+
'Tool-call pairs remain complete when a DSH message boundary differs from an OpenCode assistant part.',
|
|
16
|
+
],
|
|
17
|
+
budget(input) {
|
|
18
|
+
const reserve = input.reserveTokens ?? (input.maxOutputTokens || 32_000);
|
|
19
|
+
const usable = Math.max(0, input.contextWindow - reserve);
|
|
20
|
+
return {
|
|
21
|
+
triggerTokens: Math.floor(usable * (input.thresholdRatio ?? 1)),
|
|
22
|
+
retainTokens: input.keepRecentTokens ?? Math.min(15_000, Math.max(2_000, Math.floor(usable * 0.25))),
|
|
23
|
+
tailMode: 'maximum',
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
summaryInstructions: `Write a concise handoff using these headings in order:
|
|
27
|
+
## Objective
|
|
28
|
+
## Important Details
|
|
29
|
+
## Work State
|
|
30
|
+
### Completed
|
|
31
|
+
### Active
|
|
32
|
+
### Blocked
|
|
33
|
+
## Next Move
|
|
34
|
+
## Relevant Files
|
|
35
|
+
|
|
36
|
+
Record the user's goal, constraints, decisions, completed work, pending work, blockers, and next actions. Include exact paths, symbols, commands, and errors needed to continue. Integrate any prior summary; resolve conflicting facts using the newer conversation. Keep unfinished requests and relevant earlier decisions. Use short bullets and keep empty sections with "(none)".`,
|
|
37
|
+
maxSummaryTokens: 32_000,
|
|
38
|
+
summaryToolChars: 2_000,
|
|
39
|
+
prune: {
|
|
40
|
+
enabled: false,
|
|
41
|
+
keepRecentTools: 0,
|
|
42
|
+
keepRecentTurns: 2,
|
|
43
|
+
protectTokens: 40_000,
|
|
44
|
+
minSavingsTokens: 20_000,
|
|
45
|
+
strictSavings: true,
|
|
46
|
+
protectedTools: ['skill'],
|
|
47
|
+
},
|
|
48
|
+
};
|