billion-context-dsh 0.1.5 → 0.1.7
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/README.en.md +8 -7
- package/README.md +8 -7
- package/dist/config.d.ts +6 -2
- package/dist/index.d.ts +37 -6
- package/dist/index.js +186 -43
- package/dist/index.js.map +1 -1
- package/dist/region.d.ts +40 -12
- package/dist/system-prompt.d.ts +1 -1
- package/dist/tools.d.ts +4 -0
- package/dist/window.d.ts +32 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[English](./README.en.md) | [中文](./README.md)
|
|
4
4
|
|
|
5
5
|
> **⚠️ Beta notice — not for production use**
|
|
6
|
-
> This project (**v0.1.
|
|
6
|
+
> This project (**v0.1.7**) is a work-in-progress beta. The [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) itself is also in **public beta**. **Do not use either in engineering / production environments** — expect breaking changes and rough edges.
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
9
|
<strong>Built with gratitude on top of these projects</strong> — please give them a ⭐:
|
|
@@ -73,7 +73,7 @@ That's it. Then add a composition row where a compaction backend is expected —
|
|
|
73
73
|
- id: compaction-acp
|
|
74
74
|
name: 'billion-context-dsh'
|
|
75
75
|
config:
|
|
76
|
-
modelContextLimit: 128000 #
|
|
76
|
+
modelContextLimit: 128000 # optional; omit to auto-detect the model's real window (fallback 128000)
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
**Per-mode — an agent preset's `compaction` realm.** First *disable (or delete) the realm's existing `dsh-compaction-basic` row*, then mount this engine — two backends cannot coexist in the same realm:
|
|
@@ -87,7 +87,7 @@ That's it. Then add a composition row where a compaction backend is expected —
|
|
|
87
87
|
- id: compaction-acp
|
|
88
88
|
name: 'billion-context-dsh'
|
|
89
89
|
config:
|
|
90
|
-
modelContextLimit: 128000 #
|
|
90
|
+
modelContextLimit: 128000 # optional; omit to auto-detect the model's real window (fallback 128000)
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
> **One context manager per agent.** Two backends providing `ctx.compaction` collide — never run both in the same realm. Full install & verification guide: [docs/INSTALL.md](docs/INSTALL.md).
|
|
@@ -142,10 +142,11 @@ This project reuses `acp-kernel`'s compression core and `billion-context-pi`'s d
|
|
|
142
142
|
|
|
143
143
|
| Key | Default | Meaning |
|
|
144
144
|
|---|---|---|
|
|
145
|
-
| `modelContextLimit` | `128000` | Context window used for the kernel's pressure decisions |
|
|
146
|
-
| `
|
|
147
|
-
| `
|
|
148
|
-
| `
|
|
145
|
+
| `modelContextLimit` | auto-detected (fallback `128000`) | Context window used for the kernel's pressure decisions; an explicit value wins and skips the probe |
|
|
146
|
+
| `autoModelContextLimit` | `true` | Probe the model's real window from the model API (`agent.ctx.llm.resolveModelInfo`); fall back to the default on failure, `acp_status` shows the window source |
|
|
147
|
+
| `nudgeMinContextLimitPct` | kernel default `0.45` | Nudge window lower bound (usage fraction) — validation only; the growth-driven trigger has no percentage floor — same default as billion-context-pi |
|
|
148
|
+
| `nudgeMaxContextLimitPct` | engine default `0.70` (kernel/pi default `0.75`) | Over-limit line: above this the nudge fires regardless of growth — deliberately below the host compaction-basic 80% auto-compaction line so the forced nudge fires first; an explicit value wins |
|
|
149
|
+
| `nudgeEmergencyThresholdPct` | engine default `0.85` (kernel/pi default `0.95`) | Emergency nudge (bypasses the per-turn dedup) — lowered from `0.95`: at 95% the model has no room to act and the 80% auto-compaction line shadows it; an explicit value wins |
|
|
149
150
|
| `coreOverrides` | — | Any other acp-kernel `Config` override (billion-context-pi's `coreOverrides` escape hatch) |
|
|
150
151
|
| `autoTools` | `true` | Register the four model tools on `ctx.tools` |
|
|
151
152
|
| `autoCommand` | `true` | Register the `/acp` command on `ctx.commands` |
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[中文](./README.md) | [English](./README.en.md)
|
|
4
4
|
|
|
5
5
|
> **⚠️ 测试版声明——请勿用于生产环境**
|
|
6
|
-
> 本项目(**v0.1.
|
|
6
|
+
> 本项目(**v0.1.7**)仍处于开发中的测试版。[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 本身也处于**公开测试版**阶段。**请勿将两者用于工程化 / 生产环境**——预期会有破坏性变更与粗糙之处。
|
|
7
7
|
|
|
8
8
|
<p align="center">
|
|
9
9
|
<strong>衷心感谢以下项目——请给它们一个 ⭐:</strong>
|
|
@@ -75,7 +75,7 @@ npm install billion-context-dsh
|
|
|
75
75
|
- id: compaction-acp
|
|
76
76
|
name: 'billion-context-dsh'
|
|
77
77
|
config:
|
|
78
|
-
modelContextLimit: 128000 #
|
|
78
|
+
modelContextLimit: 128000 # 可选;省略时自动探测模型真实窗口(回退 128000)
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
**单模式生效(agent preset 的 `compaction` realm)**。先在该 realm 内*禁用(或删除)原有的 `dsh-compaction-basic` 行*,再插入本引擎——同一 realm 内两个后端不能并存:
|
|
@@ -89,7 +89,7 @@ npm install billion-context-dsh
|
|
|
89
89
|
- id: compaction-acp
|
|
90
90
|
name: 'billion-context-dsh'
|
|
91
91
|
config:
|
|
92
|
-
modelContextLimit: 128000 #
|
|
92
|
+
modelContextLimit: 128000 # 可选;省略时自动探测模型真实窗口(回退 128000)
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
> **每个 agent 只留一个上下文管理器。** 两个后端同时 provide `ctx.compaction` 会冲突——同一 realm 内切勿并存。完整安装与验证指南见 [docs/INSTALL.md](docs/INSTALL.md)。
|
|
@@ -144,10 +144,11 @@ DSH 的每个模型请求都派生自其 append-only 会话日志(*surface*)
|
|
|
144
144
|
|
|
145
145
|
| 键 | 默认值 | 含义 |
|
|
146
146
|
|---|---|---|
|
|
147
|
-
| `modelContextLimit` | `128000
|
|
148
|
-
| `
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
147
|
+
| `modelContextLimit` | 自动探测(回退 `128000`) | 用于内核压力决策的上下文窗口;显式配置时优先且跳过探测 |
|
|
148
|
+
| `autoModelContextLimit` | `true` | 从模型 API 自动探测真实窗口(`agent.ctx.llm.resolveModelInfo`);探测失败回退默认值,`acp_status` 展示窗口来源 |
|
|
149
|
+
| `nudgeMinContextLimitPct` | 内核默认 `0.45` | Nudge 窗口下界(用量占比)——仅作配置校验,增长路径的触发没有百分比下限——与 billion-context-pi 相同的默认值 |
|
|
150
|
+
| `nudgeMaxContextLimitPct` | engine 默认 `0.70`(内核/pi 默认 `0.75`) | 过限线:超过此值则无论增长与否都触发 nudge——刻意低于宿主 compaction-basic 的 80% 自动压缩线,保证强制 nudge 先触发;显式配置优先 |
|
|
151
|
+
| `nudgeEmergencyThresholdPct` | engine 默认 `0.85`(内核/pi 默认 `0.95`) | 紧急 nudge(绕过每轮去重)——从 `0.95` 下调:95% 时模型已无操作空间且会被 80% 自动压缩线遮蔽;显式配置优先 |
|
|
151
152
|
| `coreOverrides` | — | 任何其他 acp-kernel `Config` 覆盖(billion-context-pi 的 `coreOverrides` 逃生口) |
|
|
152
153
|
| `autoTools` | `true` | 在 `ctx.tools` 注册四个模型工具 |
|
|
153
154
|
| `autoCommand` | `true` | 在 `ctx.commands` 注册 `/acp` 命令 |
|
package/dist/config.d.ts
CHANGED
|
@@ -7,15 +7,19 @@
|
|
|
7
7
|
* growth ratio 5%, protected last messages 5). Every knob is optional — an
|
|
8
8
|
* omitted value keeps the kernel default, so the behavior matches the Pi
|
|
9
9
|
* adapter exactly unless a deployment opts out.
|
|
10
|
+
*
|
|
11
|
+
* NOTE: `AcpCompactionEngine` (src/index.ts) ships its own engine-level
|
|
12
|
+
* defaults 0.70/0.85 for the two nudge thresholds on top of this layer, so an
|
|
13
|
+
* engine with no explicit config lands on 0.70/0.85, not 0.75/0.95.
|
|
10
14
|
* @module billion-context-dsh/config
|
|
11
15
|
*/
|
|
12
16
|
import { type Config } from 'acp-kernel';
|
|
13
17
|
/** The kernel-facing knobs shared by the nudge path and the compress tool. */
|
|
14
18
|
export interface KernelConfigInput {
|
|
15
19
|
readonly modelContextLimit: number;
|
|
16
|
-
/** Nudge window lower bound (usage fraction). Kernel default: 0.45. */
|
|
20
|
+
/** Nudge window lower bound (usage fraction; validation only — the growth-driven trigger has no percentage floor). Kernel default: 0.45. */
|
|
17
21
|
readonly nudgeMinContextLimitPct?: number;
|
|
18
|
-
/** Nudge window upper bound — over-limit. Kernel default: 0.75. */
|
|
22
|
+
/** Nudge window upper bound — over-limit guarantee line. Kernel default: 0.75. */
|
|
19
23
|
readonly nudgeMaxContextLimitPct?: number;
|
|
20
24
|
/** Emergency nudge threshold (bypasses per-turn dedup). Kernel default: 0.95. */
|
|
21
25
|
readonly nudgeEmergencyThresholdPct?: number;
|
package/dist/index.d.ts
CHANGED
|
@@ -28,23 +28,44 @@
|
|
|
28
28
|
import type { Context } from '@deepseek-ai/cordis';
|
|
29
29
|
import { CompactionEngine, type CompactionAgentContext, type CompactionResult, type CompactionTrigger, type ManualCompactAgentContext } from '@deepseek-ai/dsh-compaction';
|
|
30
30
|
import { type CompressionCore } from 'acp-kernel';
|
|
31
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
31
32
|
import { AcpStateStore } from './state.ts';
|
|
33
|
+
import { type AcpWindow } from './window.ts';
|
|
32
34
|
export { AcpStateStore } from './state.ts';
|
|
33
35
|
export { kernelConfigFor, type KernelConfigInput } from './config.ts';
|
|
34
36
|
export { ACP_SYSTEM_PROMPT, ACP_SYSTEM_PROMPT_ORDER } from './system-prompt.ts';
|
|
35
37
|
export { makeTools, type ToolEnvironment } from './tools.ts';
|
|
36
38
|
export { acpCommand } from './commands.ts';
|
|
37
39
|
export { buildNudge, type NudgeEnvironment, type NudgeOutcome } from './nudge.ts';
|
|
38
|
-
export {
|
|
40
|
+
export { DEFAULT_CONTEXT_WINDOW, detectContextWindow, windowSourceLabel, type AcpWindow, } from './window.ts';
|
|
41
|
+
export { AlreadyCompressedRangeError, rebuildBlockLedger, resolveSurfaceRange, runCompactionTransaction, shadowedSeqsOf, findOpenTurn, assertNoActiveCompaction, blockRegistry, blockRefForSummarySeq, compactionIdsOfKernelBlocks, summarySeqOfKernelBlock, expandShadowedSeqs, type AcpBlockLedgerEntry, type CompactionTransactionInput, type ResolvedSurfaceRange, } from './region.ts';
|
|
39
42
|
export { eventsToCoreMessages, projectEvent, surfaceEventsOf, extractEventText } from './messages.ts';
|
|
40
43
|
export interface AcpConfig {
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
/**
|
|
45
|
+
* The context window used for pressure decisions, in tokens. When omitted,
|
|
46
|
+
* `autoModelContextLimit` (default true) probes the model's real window via
|
|
47
|
+
* `agent.ctx.llm.resolveModelInfo(provider, model)`; an explicit value
|
|
48
|
+
* always wins and disables the probe.
|
|
49
|
+
*/
|
|
50
|
+
readonly modelContextLimit?: number;
|
|
51
|
+
/** Probe the model's real context window from the LLM runtime. Default true. */
|
|
52
|
+
readonly autoModelContextLimit: boolean;
|
|
53
|
+
/** Nudge window lower bound (usage fraction; validation only — the growth-driven trigger has no percentage floor). Kernel default 0.45 — same as billion-context-pi. */
|
|
44
54
|
readonly nudgeMinContextLimitPct?: number;
|
|
45
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Nudge window upper bound — over-limit guarantee line: above this the
|
|
57
|
+
* kernel injects a nudge regardless of growth or cadence. Engine default
|
|
58
|
+
* 0.70 (deliberately BELOW the kernel/billion-context-pi default 0.75 and
|
|
59
|
+
* the host compaction-basic auto-compaction line 0.80, so the forced nudge
|
|
60
|
+
* always fires first); an explicit value wins.
|
|
61
|
+
*/
|
|
46
62
|
readonly nudgeMaxContextLimitPct?: number;
|
|
47
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Emergency nudge threshold (bypasses the per-turn dedup). Engine default
|
|
65
|
+
* 0.85 (down from the kernel/billion-context-pi default 0.95: 95% leaves
|
|
66
|
+
* the model no room to act before the API rejects, and the host's 80%
|
|
67
|
+
* compaction-basic line shadows it in standard/code/cordis modes).
|
|
68
|
+
*/
|
|
48
69
|
readonly nudgeEmergencyThresholdPct?: number;
|
|
49
70
|
/** Any other acp-kernel Config override (billion-context-pi's `coreOverrides` escape hatch). */
|
|
50
71
|
readonly coreOverrides?: Partial<import('acp-kernel').Config>;
|
|
@@ -69,7 +90,17 @@ export declare class AcpCompactionEngine extends CompactionEngine {
|
|
|
69
90
|
/** Resolved engine configuration. */
|
|
70
91
|
readonly config: AcpConfig;
|
|
71
92
|
private readonly lastNudgeTurn;
|
|
93
|
+
/** Per provider/model route the resolved window (probe failures cached too). */
|
|
94
|
+
private readonly windowCache;
|
|
72
95
|
constructor(ctx: Context, config?: Partial<AcpConfig>);
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the effective context window for an agent. An explicitly
|
|
98
|
+
* configured `modelContextLimit` always wins (no probe). Otherwise probe the
|
|
99
|
+
* model's real window via `agent.ctx.llm.resolveModelInfo` (cached per
|
|
100
|
+
* provider/model route, probe failures cached too) and fall back to
|
|
101
|
+
* DEFAULT_CONTEXT_WINDOW when auto-detection is disabled or unavailable.
|
|
102
|
+
*/
|
|
103
|
+
windowFor(agent: Agent): Promise<AcpWindow>;
|
|
73
104
|
/** ACP is model-driven: automatic pressure policy never summarizes by itself. */
|
|
74
105
|
compactIfNeeded(_agent: CompactionAgentContext, _trigger: CompactionTrigger, signal: AbortSignal): Promise<CompactionResult | null>;
|
|
75
106
|
/** Explicit idle-session compaction: ACP leaves the decision to the model. */
|
package/dist/index.js
CHANGED
|
@@ -163,14 +163,61 @@ function hasPlainRef(session, seq) {
|
|
|
163
163
|
return false;
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
+
var AlreadyCompressedRangeError = class extends Error {
|
|
167
|
+
constructor(start, end, coveringBlockIds) {
|
|
168
|
+
super(
|
|
169
|
+
`billion-context-dsh: seq ${start}..${end} already compressed \u2014 no live content remains in that span`
|
|
170
|
+
);
|
|
171
|
+
this.start = start;
|
|
172
|
+
this.end = end;
|
|
173
|
+
this.coveringBlockIds = coveringBlockIds;
|
|
174
|
+
this.name = "AlreadyCompressedRangeError";
|
|
175
|
+
}
|
|
176
|
+
start;
|
|
177
|
+
end;
|
|
178
|
+
coveringBlockIds;
|
|
179
|
+
};
|
|
180
|
+
function recoverStaleRange(session, start, end) {
|
|
181
|
+
if (session.events[start] === void 0 || session.events[end] === void 0) {
|
|
182
|
+
const failedEdge = session.events[start] === void 0 ? start : end;
|
|
183
|
+
return { kind: "unresolvable", failedEdge };
|
|
184
|
+
}
|
|
185
|
+
const liveInside = session.surface.nodes.filter((seq) => seq >= start && seq <= end).sort((a, b) => a - b);
|
|
186
|
+
const plain = liveInside.filter((seq) => !isCheckpointNode(session.events[seq]));
|
|
187
|
+
if (plain.length === 0) {
|
|
188
|
+
const coveringBlockIds = rebuildBlockLedger(session.events).filter((entry) => entry.shadowedSeqs.some((seq) => seq >= start && seq <= end)).map((entry) => entry.blockId);
|
|
189
|
+
return { kind: "already-compressed", coveringBlockIds };
|
|
190
|
+
}
|
|
191
|
+
return { kind: "ok", start: plain[0], end: plain[plain.length - 1] };
|
|
192
|
+
}
|
|
166
193
|
function resolveSurfaceRange(session, start, end) {
|
|
167
194
|
const nodes = session.surface.nodes;
|
|
168
|
-
|
|
169
|
-
|
|
195
|
+
if (start > end) {
|
|
196
|
+
throw new Error(`billion-context-dsh: reversed range ${start}..${end}`);
|
|
197
|
+
}
|
|
198
|
+
let requestedStartIdx = nodes.indexOf(start);
|
|
199
|
+
let requestedEndIdx = nodes.indexOf(end);
|
|
200
|
+
let recovered = false;
|
|
170
201
|
if (requestedStartIdx < 0 || requestedEndIdx < 0) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
202
|
+
const stale = recoverStaleRange(session, start, end);
|
|
203
|
+
if (stale.kind === "unresolvable") {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`billion-context-dsh: seq ${start}..${end} not in the current surface \u2014 edge seq ${stale.failedEdge} is not in this session's log. Surface seqs are sparse message nodes (only user/message, assistant/message, tool/result events); consult acp_status for the current surface range`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (stale.kind === "already-compressed") {
|
|
209
|
+
throw new AlreadyCompressedRangeError(start, end, stale.coveringBlockIds);
|
|
210
|
+
}
|
|
211
|
+
start = stale.start;
|
|
212
|
+
end = stale.end;
|
|
213
|
+
recovered = true;
|
|
214
|
+
requestedStartIdx = nodes.indexOf(start);
|
|
215
|
+
requestedEndIdx = nodes.indexOf(end);
|
|
216
|
+
if (requestedStartIdx < 0 || requestedEndIdx < 0) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`billion-context-dsh: seq ${start}..${end} not in the current surface \u2014 consult acp_status for the current surface range`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
174
221
|
}
|
|
175
222
|
if (requestedStartIdx > requestedEndIdx) {
|
|
176
223
|
throw new Error(`billion-context-dsh: reversed range ${start}..${end}`);
|
|
@@ -189,7 +236,12 @@ function resolveSurfaceRange(session, start, end) {
|
|
|
189
236
|
endIdx -= 1;
|
|
190
237
|
}
|
|
191
238
|
if (startIdx <= endIdx && nodes[startIdx] <= nodes[endIdx]) {
|
|
192
|
-
return { start: nodes[startIdx], end: nodes[endIdx] };
|
|
239
|
+
return recovered ? { start: nodes[startIdx], end: nodes[endIdx], recovered: true } : { start: nodes[startIdx], end: nodes[endIdx] };
|
|
240
|
+
}
|
|
241
|
+
if (recovered) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`billion-context-dsh: no tool-pairing-balanced live remainder around seq ${start}..${end} \u2014 narrow the range or consult acp_status for the current surface`
|
|
244
|
+
);
|
|
193
245
|
}
|
|
194
246
|
startIdx = requestedStartIdx;
|
|
195
247
|
endIdx = requestedEndIdx;
|
|
@@ -528,6 +580,28 @@ function kernelConfigFor(input) {
|
|
|
528
580
|
return defaultConfig(input.modelContextLimit, overrides);
|
|
529
581
|
}
|
|
530
582
|
|
|
583
|
+
// src/window.ts
|
|
584
|
+
var DEFAULT_CONTEXT_WINDOW = 128e3;
|
|
585
|
+
function windowSourceLabel(window) {
|
|
586
|
+
if (window.source === "explicit") return "configured";
|
|
587
|
+
if (window.source === "auto") {
|
|
588
|
+
return `auto-detected from ${window.provider ?? "?"}/${window.model ?? "?"}`;
|
|
589
|
+
}
|
|
590
|
+
return "default (auto-detection unavailable)";
|
|
591
|
+
}
|
|
592
|
+
async function detectContextWindow(agent, provider, model) {
|
|
593
|
+
const llm = agent.ctx?.get?.("llm");
|
|
594
|
+
if (llm?.resolveModelInfo === void 0) return null;
|
|
595
|
+
try {
|
|
596
|
+
const info = await llm.resolveModelInfo(provider, model);
|
|
597
|
+
const window = info?.context?.contextWindow;
|
|
598
|
+
if (typeof window === "number" && Number.isInteger(window) && window > 0) return window;
|
|
599
|
+
return null;
|
|
600
|
+
} catch {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
531
605
|
// src/tools.ts
|
|
532
606
|
function textOutput() {
|
|
533
607
|
return {
|
|
@@ -592,30 +666,51 @@ async function handleCompress(env, args, exec) {
|
|
|
592
666
|
const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
|
|
593
667
|
env.store.set(session, turn.state);
|
|
594
668
|
const byRaw = turn.state.messageRefs.byRaw;
|
|
595
|
-
const ranges =
|
|
669
|
+
const ranges = [];
|
|
670
|
+
const alreadyCompressedNotes = [];
|
|
671
|
+
for (const range of args.content) {
|
|
596
672
|
const startSeq = parseSeq(range.startSeq);
|
|
597
673
|
const endSeq = parseSeq(range.endSeq);
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
674
|
+
let resolved;
|
|
675
|
+
try {
|
|
676
|
+
resolved = resolveSurfaceRange(session, startSeq, endSeq);
|
|
677
|
+
} catch (error) {
|
|
678
|
+
if (error instanceof AlreadyCompressedRangeError) {
|
|
679
|
+
const covering = error.coveringBlockIds;
|
|
680
|
+
const blockNote = covering.length === 0 ? "" : ` (block ${covering[0].slice(0, 8)}${covering.length > 1 ? ` +${covering.length - 1} more` : ""})`;
|
|
681
|
+
alreadyCompressedNotes.push(
|
|
682
|
+
` seqs ${error.start}..${error.end} already compressed${blockNote} \u2014 nothing to reclaim; decompress to recover the originals`
|
|
683
|
+
);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
throw error;
|
|
687
|
+
}
|
|
688
|
+
const startBlockRef = blockRefForSummarySeq(session, resolved.start);
|
|
689
|
+
const endBlockRef = blockRefForSummarySeq(session, resolved.end);
|
|
690
|
+
const startRef = startBlockRef ?? byRaw[String(resolved.start)];
|
|
691
|
+
const endRef = endBlockRef ?? byRaw[String(resolved.end)];
|
|
603
692
|
if (startRef === void 0 || endRef === void 0) {
|
|
604
693
|
throw new Error(
|
|
605
|
-
`billion-context-dsh: seq ${start}..${end} has no assigned ref \u2014 the range must be on the current surface (run acp_status for the live seq list)`
|
|
694
|
+
`billion-context-dsh: seq ${resolved.start}..${resolved.end} has no assigned ref \u2014 the range must be on the current surface (run acp_status for the live seq list)`
|
|
606
695
|
);
|
|
607
696
|
}
|
|
608
|
-
|
|
697
|
+
ranges.push({
|
|
698
|
+
...resolved,
|
|
609
699
|
startSeq,
|
|
610
700
|
endSeq,
|
|
611
|
-
start,
|
|
612
|
-
end,
|
|
613
701
|
startRef,
|
|
614
702
|
endRef,
|
|
615
703
|
summary: range.summary,
|
|
616
704
|
...(range.topic ?? args.topic) === void 0 ? {} : { topic: range.topic ?? args.topic }
|
|
617
|
-
};
|
|
618
|
-
}
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
if (ranges.length === 0) {
|
|
708
|
+
const text = ["Compressed 0 block(s), ~0 tokens reclaimed.", ...alreadyCompressedNotes];
|
|
709
|
+
if (alreadyCompressedNotes.length > 0) {
|
|
710
|
+
text.push(" (all requested ranges were already compressed \u2014 decompress a block to recover its originals)");
|
|
711
|
+
}
|
|
712
|
+
return { text: text.join("\n") };
|
|
713
|
+
}
|
|
619
714
|
const applied = env.kernel.applyCompression({
|
|
620
715
|
ranges: ranges.map(({ startRef, endRef, summary, topic }) => ({ startRef, endRef, summary, topic })),
|
|
621
716
|
messages: coreMessages,
|
|
@@ -648,9 +743,7 @@ async function handleCompress(env, args, exec) {
|
|
|
648
743
|
}
|
|
649
744
|
const lines = [];
|
|
650
745
|
let skippedRanges = 0;
|
|
651
|
-
for (
|
|
652
|
-
const range = ranges[index];
|
|
653
|
-
const original = args.content[index];
|
|
746
|
+
for (const range of ranges) {
|
|
654
747
|
const key = `${range.startRef}::${range.endRef}`;
|
|
655
748
|
const block = blockByRangeKey.get(key);
|
|
656
749
|
if (block === void 0) {
|
|
@@ -672,7 +765,7 @@ async function handleCompress(env, args, exec) {
|
|
|
672
765
|
start,
|
|
673
766
|
end,
|
|
674
767
|
shadowedSeqs: shadowed,
|
|
675
|
-
summary: [{ type: "text", text:
|
|
768
|
+
summary: [{ type: "text", text: range.summary }],
|
|
676
769
|
shadowedTokenCount: shadowedTokens,
|
|
677
770
|
provider: agent.options.provider ?? "",
|
|
678
771
|
model: agent.options.model ?? "",
|
|
@@ -687,13 +780,15 @@ async function handleCompress(env, args, exec) {
|
|
|
687
780
|
});
|
|
688
781
|
const adjusted = start !== range.startSeq || end !== range.endSeq;
|
|
689
782
|
const tierLabel = tier === 1 ? "" : `, tier ${tier}`;
|
|
783
|
+
const note = range.recovered === true ? ` (seqs ${range.startSeq}..${range.endSeq} were already shadowed \u2014 compressed the live remainder ${start}..${end})` : adjusted ? ` (adjusted from ${range.startSeq}..${range.endSeq} to balanced edges)` : "";
|
|
690
784
|
lines.push(
|
|
691
|
-
` block ${compactionId.slice(0, 8)}: seqs ${start}..${end}, ${shadowed.length} messages shadowed${tierLabel}
|
|
785
|
+
` block ${compactionId.slice(0, 8)}: seqs ${start}..${end}, ${shadowed.length} messages shadowed${tierLabel}${note}`
|
|
692
786
|
);
|
|
693
787
|
}
|
|
694
788
|
const summaryLine = `Compressed ${applied.result.blocksCreated} block(s), ~${applied.result.tokensCompressed} tokens reclaimed.`;
|
|
695
|
-
const
|
|
696
|
-
const
|
|
789
|
+
const totalSkipped = skippedRanges + alreadyCompressedNotes.length;
|
|
790
|
+
const warningLines = [...freeWarnings.map((warning) => ` ${warning}`), ...alreadyCompressedNotes, ...lines];
|
|
791
|
+
const footer = totalSkipped > 0 ? ` (${totalSkipped} range(s) skipped \u2014 see warnings above)` : "";
|
|
697
792
|
return { text: `${summaryLine}
|
|
698
793
|
${[...warningLines, footer].filter((line) => line !== "").join("\n")}` };
|
|
699
794
|
}
|
|
@@ -746,18 +841,21 @@ ${original}`.toLowerCase();
|
|
|
746
841
|
};
|
|
747
842
|
}
|
|
748
843
|
var statusParameters = {};
|
|
749
|
-
function handleStatus(env, _args, exec) {
|
|
750
|
-
const
|
|
844
|
+
async function handleStatus(env, _args, exec) {
|
|
845
|
+
const agent = requireAgent(exec);
|
|
846
|
+
const session = agent.session;
|
|
751
847
|
const ledger = rebuildBlockLedger(session.events);
|
|
752
848
|
const totalTokens = ledger.reduce((sum, block) => sum + block.shadowedTokenCount, 0);
|
|
753
849
|
const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
|
|
754
850
|
const estimated = coreMessages.reduce((sum, message) => sum + defaultCountTokens2(message.text ?? ""), 0);
|
|
755
|
-
const
|
|
851
|
+
const window = env.windowFor === void 0 ? { limit: env.modelContextLimit, source: "explicit" } : await env.windowFor(agent);
|
|
852
|
+
const limit = window.limit;
|
|
756
853
|
const lines = [
|
|
757
854
|
`ACP status \u2014 session ${session.id}`,
|
|
758
855
|
` blocks: ${ledger.length}`,
|
|
759
856
|
` tokens compressed: ${totalTokens}`,
|
|
760
857
|
` estimated context: ${estimated} / ${limit} (${Math.round(estimated / limit * 100)}%)`,
|
|
858
|
+
` context window: ${limit} (${windowSourceLabel(window)})`,
|
|
761
859
|
` surface: ${surfaceSummary(session)}`
|
|
762
860
|
];
|
|
763
861
|
for (const block of ledger.slice(0, 10)) {
|
|
@@ -769,7 +867,7 @@ function makeTools(env) {
|
|
|
769
867
|
return [
|
|
770
868
|
defineTool({
|
|
771
869
|
name: "compress",
|
|
772
|
-
description: "Replace older conversation ranges with dense summaries you write. Each message seq is a surface reference. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated ranges in one call (each content entry becomes its own block); keep ranges disjoint. Never compress content the current step is actively using.",
|
|
870
|
+
description: "Replace older conversation ranges with dense summaries you write. Each message seq is a surface reference. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated ranges in one call (each content entry becomes its own block); keep ranges disjoint. Never compress content the current step is actively using. Seq refs must come from the CURRENT surface (acp_status or the latest nudge): a span whose edges were shadowed by an earlier compress is auto-remapped to its still-live content, a fully compressed span is reported as already compressed, and invented/other-session seqs fail with guidance.",
|
|
773
871
|
parameters: compressParameters,
|
|
774
872
|
output: textOutput(),
|
|
775
873
|
async execute(args, exec) {
|
|
@@ -800,7 +898,7 @@ function makeTools(env) {
|
|
|
800
898
|
parameters: statusParameters,
|
|
801
899
|
output: textOutput(),
|
|
802
900
|
execute(args, exec) {
|
|
803
|
-
return
|
|
901
|
+
return handleStatus(env, args, exec);
|
|
804
902
|
}
|
|
805
903
|
})
|
|
806
904
|
];
|
|
@@ -808,18 +906,20 @@ function makeTools(env) {
|
|
|
808
906
|
|
|
809
907
|
// src/commands.ts
|
|
810
908
|
import { defaultCountTokens as defaultCountTokens3 } from "acp-kernel";
|
|
811
|
-
function statusText(env, agent) {
|
|
909
|
+
async function statusText(env, agent) {
|
|
812
910
|
const session = agent.session;
|
|
813
911
|
const ledger = rebuildBlockLedger(session.events);
|
|
814
912
|
const totalTokens = ledger.reduce((sum, block) => sum + block.shadowedTokenCount, 0);
|
|
815
913
|
const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
|
|
816
914
|
const estimated = coreMessages.reduce((sum, message) => sum + defaultCountTokens3(message.text ?? ""), 0);
|
|
817
|
-
const
|
|
915
|
+
const window = env.windowFor === void 0 ? { limit: env.modelContextLimit, source: "explicit" } : await env.windowFor(agent);
|
|
916
|
+
const limit = window.limit;
|
|
818
917
|
const lines = [
|
|
819
918
|
`ACP status \u2014 session ${session.id}`,
|
|
820
919
|
` blocks: ${ledger.length}`,
|
|
821
920
|
` tokens compressed: ${totalTokens}`,
|
|
822
|
-
` estimated context: ${estimated} / ${limit} (${Math.round(estimated / limit * 100)}%)
|
|
921
|
+
` estimated context: ${estimated} / ${limit} (${Math.round(estimated / limit * 100)}%)`,
|
|
922
|
+
` context window: ${limit} (${windowSourceLabel(window)})`
|
|
823
923
|
];
|
|
824
924
|
for (const block of ledger.slice(0, 10)) {
|
|
825
925
|
const tier = block.tier > 1 ? ` [T${block.tier}]` : "";
|
|
@@ -877,7 +977,7 @@ function acpCommand(env) {
|
|
|
877
977
|
handler: async (invocation) => {
|
|
878
978
|
const raw = invocation.rawInput.trim();
|
|
879
979
|
if (raw === "" || raw === "status") {
|
|
880
|
-
return { kind: "success", text: statusText(env, invocation.agent) };
|
|
980
|
+
return { kind: "success", text: await statusText(env, invocation.agent) };
|
|
881
981
|
}
|
|
882
982
|
if (raw.startsWith("compress")) {
|
|
883
983
|
return { kind: "success", text: compressText(env, invocation.agent, raw.slice("compress".length).trim().split(/\s+/)) };
|
|
@@ -904,7 +1004,8 @@ function rangeTable(session) {
|
|
|
904
1004
|
`Surface: ${surfaceSummary(session)}`,
|
|
905
1005
|
"Compressible ranges (suggestions only \u2014 compress any consumed span; refs are surface seqs):",
|
|
906
1006
|
...lines,
|
|
907
|
-
"Compress with: compress({ content: [{ startSeq, endSeq, summary }] }) \u2014 content is an array: batch multiple unrelated segments in one call, each entry its own block. Keep ranges disjoint."
|
|
1007
|
+
"Compress with: compress({ content: [{ startSeq, endSeq, summary }] }) \u2014 content is an array: batch multiple unrelated segments in one call, each entry its own block. Keep ranges disjoint.",
|
|
1008
|
+
"Snapshot taken at nudge time: the seqs go stale once the surface moves (a later compress shadows them), so re-run acp_status for fresh refs before compressing."
|
|
908
1009
|
].join("\n");
|
|
909
1010
|
}
|
|
910
1011
|
function measuredTokenCount(agent, coreMessages) {
|
|
@@ -963,10 +1064,10 @@ YOU decide whether and when to compress context. Nothing forces you: the injecte
|
|
|
963
1064
|
${COMPRESS_PHILOSOPHY}
|
|
964
1065
|
|
|
965
1066
|
Compression tools (refs are SURFACE SEQS, not ids):
|
|
966
|
-
- compress: replace one or more seq ranges, each with your own dense summary. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated segments in one call (each entry becomes its own block): compress({ content: [{ startSeq: 1, endSeq: 5, summary: '...' }, { startSeq: 12, endSeq: 18, summary: '...' }] }). Keep ranges disjoint \u2014 overlapping entries in one batch are skipped. Edges are auto-balanced to tool-call/result boundaries; a trailing #callId fragment in a seq is ignored.
|
|
1067
|
+
- compress: replace one or more seq ranges, each with your own dense summary. Single range: compress({ content: [{ startSeq, endSeq, summary }] }). Batch multiple unrelated segments in one call (each entry becomes its own block): compress({ content: [{ startSeq: 1, endSeq: 5, summary: '...' }, { startSeq: 12, endSeq: 18, summary: '...' }] }). Keep ranges disjoint \u2014 overlapping entries in one batch are skipped. Edges are auto-balanced to tool-call/result boundaries; a trailing #callId fragment in a seq is ignored. Seq refs must be on the current surface: seqs from older nudges or earlier compresses go stale as the surface moves, so a stale span is auto-remapped to its still-live remainder (the result reports the adjusted span), a fully compressed span is reported as already compressed, and invented/other-session seqs fail with guidance.
|
|
967
1068
|
- decompress: recover a compressed block's original content, read-only. decompress({ blockId }).
|
|
968
1069
|
- search_context: find information inside compressed blocks BEFORE decompressing. search_context({ query }).
|
|
969
|
-
- acp_status: current context usage and the live compressible-range list. Run it before compressing
|
|
1070
|
+
- acp_status: current context usage and the live compressible-range list. Run it right before compressing \u2014 the only seqs that never go stale are the ones you just read.
|
|
970
1071
|
|
|
971
1072
|
Tiered compression: each compressed block appears on the surface as one summary node. Compressing that node again DISTILLS the block (tier 2): the parent summary folds into your new summary and the original messages are freed. Distilling a tier-2 block yields tier 3. Distill when a summary itself is consumed \u2014 decompress on the tier-2 block recovers the full originals.
|
|
972
1073
|
|
|
@@ -975,10 +1076,17 @@ var ACP_SYSTEM_PROMPT_ORDER = 150;
|
|
|
975
1076
|
|
|
976
1077
|
// src/index.ts
|
|
977
1078
|
var DEFAULT_CONFIG = {
|
|
978
|
-
|
|
1079
|
+
autoModelContextLimit: true,
|
|
979
1080
|
autoTools: true,
|
|
980
1081
|
autoCommand: true,
|
|
981
|
-
autoNudge: true
|
|
1082
|
+
autoNudge: true,
|
|
1083
|
+
// Nudge thresholds: engine defaults 0.70/0.85 — deliberately below the
|
|
1084
|
+
// kernel/billion-context-pi 0.75/0.95. 0.95 leaves no room to act before
|
|
1085
|
+
// the API rejects, and the host's compaction-basic line (thresholdRatio
|
|
1086
|
+
// 0.80) shadows it in standard/code/cordis modes; 0.70 keeps the forced
|
|
1087
|
+
// over-limit nudge ahead of that 80% line. Explicit values always win.
|
|
1088
|
+
nudgeMaxContextLimitPct: 0.7,
|
|
1089
|
+
nudgeEmergencyThresholdPct: 0.85
|
|
982
1090
|
};
|
|
983
1091
|
function resolveAcpConfig(config = {}) {
|
|
984
1092
|
return { ...DEFAULT_CONFIG, ...config };
|
|
@@ -991,6 +1099,8 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
991
1099
|
/** Resolved engine configuration. */
|
|
992
1100
|
config;
|
|
993
1101
|
lastNudgeTurn = /* @__PURE__ */ new Map();
|
|
1102
|
+
/** Per provider/model route the resolved window (probe failures cached too). */
|
|
1103
|
+
windowCache = /* @__PURE__ */ new Map();
|
|
994
1104
|
constructor(ctx, config = {}) {
|
|
995
1105
|
super(ctx);
|
|
996
1106
|
this.config = resolveAcpConfig(config);
|
|
@@ -999,11 +1109,13 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
999
1109
|
const env = {
|
|
1000
1110
|
kernel: this.kernel,
|
|
1001
1111
|
store: this.store,
|
|
1002
|
-
|
|
1112
|
+
// Initial value before any probe; windowFor() replaces it per pre-step.
|
|
1113
|
+
modelContextLimit: this.config.modelContextLimit ?? DEFAULT_CONTEXT_WINDOW,
|
|
1003
1114
|
nudgeMinContextLimitPct: this.config.nudgeMinContextLimitPct,
|
|
1004
1115
|
nudgeMaxContextLimitPct: this.config.nudgeMaxContextLimitPct,
|
|
1005
1116
|
nudgeEmergencyThresholdPct: this.config.nudgeEmergencyThresholdPct,
|
|
1006
|
-
coreOverrides: this.config.coreOverrides
|
|
1117
|
+
coreOverrides: this.config.coreOverrides,
|
|
1118
|
+
windowFor: (agent) => this.windowFor(agent)
|
|
1007
1119
|
};
|
|
1008
1120
|
const tools = ctx.get("tools");
|
|
1009
1121
|
if (tools !== void 0) {
|
|
@@ -1041,7 +1153,8 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
1041
1153
|
ctx.on("agent/pre-step", async (payload, next) => {
|
|
1042
1154
|
const decision = await next();
|
|
1043
1155
|
if (decision.kind === "reject") return decision;
|
|
1044
|
-
const
|
|
1156
|
+
const window = await this.windowFor(payload.agent);
|
|
1157
|
+
const outcome = buildNudge(payload.agent, { ...env, modelContextLimit: window.limit }, this.lastNudgeTurn);
|
|
1045
1158
|
if (outcome === null) return decision;
|
|
1046
1159
|
return { kind: "enter", messages: [...decision.messages, outcome.message] };
|
|
1047
1160
|
});
|
|
@@ -1055,6 +1168,32 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
1055
1168
|
});
|
|
1056
1169
|
}
|
|
1057
1170
|
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Resolve the effective context window for an agent. An explicitly
|
|
1173
|
+
* configured `modelContextLimit` always wins (no probe). Otherwise probe the
|
|
1174
|
+
* model's real window via `agent.ctx.llm.resolveModelInfo` (cached per
|
|
1175
|
+
* provider/model route, probe failures cached too) and fall back to
|
|
1176
|
+
* DEFAULT_CONTEXT_WINDOW when auto-detection is disabled or unavailable.
|
|
1177
|
+
*/
|
|
1178
|
+
async windowFor(agent) {
|
|
1179
|
+
if (this.config.modelContextLimit !== void 0) {
|
|
1180
|
+
return { limit: this.config.modelContextLimit, source: "explicit" };
|
|
1181
|
+
}
|
|
1182
|
+
const provider = agent.options.provider ?? "";
|
|
1183
|
+
const model = agent.options.model ?? "";
|
|
1184
|
+
const key = `${provider}\0${model}`;
|
|
1185
|
+
const cached = this.windowCache.get(key);
|
|
1186
|
+
if (cached !== void 0) return cached;
|
|
1187
|
+
let window;
|
|
1188
|
+
if (!this.config.autoModelContextLimit) {
|
|
1189
|
+
window = { limit: DEFAULT_CONTEXT_WINDOW, source: "default", provider, model };
|
|
1190
|
+
} else {
|
|
1191
|
+
const detected = await detectContextWindow(agent, provider, model);
|
|
1192
|
+
window = detected === null ? { limit: DEFAULT_CONTEXT_WINDOW, source: "default", provider, model } : { limit: detected, source: "auto", provider, model };
|
|
1193
|
+
}
|
|
1194
|
+
this.windowCache.set(key, window);
|
|
1195
|
+
return window;
|
|
1196
|
+
}
|
|
1058
1197
|
/** ACP is model-driven: automatic pressure policy never summarizes by itself. */
|
|
1059
1198
|
async compactIfNeeded(_agent, _trigger, signal) {
|
|
1060
1199
|
signal.throwIfAborted();
|
|
@@ -1084,6 +1223,8 @@ export {
|
|
|
1084
1223
|
ACP_SYSTEM_PROMPT_ORDER,
|
|
1085
1224
|
AcpCompactionEngine,
|
|
1086
1225
|
AcpStateStore,
|
|
1226
|
+
AlreadyCompressedRangeError,
|
|
1227
|
+
DEFAULT_CONTEXT_WINDOW,
|
|
1087
1228
|
acpCommand,
|
|
1088
1229
|
assertNoActiveCompaction,
|
|
1089
1230
|
blockRefForSummarySeq,
|
|
@@ -1091,6 +1232,7 @@ export {
|
|
|
1091
1232
|
buildNudge,
|
|
1092
1233
|
compactionIdsOfKernelBlocks,
|
|
1093
1234
|
index_default as default,
|
|
1235
|
+
detectContextWindow,
|
|
1094
1236
|
eventsToCoreMessages,
|
|
1095
1237
|
expandShadowedSeqs,
|
|
1096
1238
|
extractEventText,
|
|
@@ -1104,6 +1246,7 @@ export {
|
|
|
1104
1246
|
runCompactionTransaction,
|
|
1105
1247
|
shadowedSeqsOf,
|
|
1106
1248
|
summarySeqOfKernelBlock,
|
|
1107
|
-
surfaceEventsOf
|
|
1249
|
+
surfaceEventsOf,
|
|
1250
|
+
windowSourceLabel
|
|
1108
1251
|
};
|
|
1109
1252
|
//# sourceMappingURL=index.js.map
|