billion-context-dsh 0.1.0
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 +113 -0
- package/README.zh-CN.md +116 -0
- package/dist/commands.d.ts +9 -0
- package/dist/config.d.ts +30 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +724 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +38 -0
- package/dist/nudge.d.ts +29 -0
- package/dist/region.d.ts +61 -0
- package/dist/state.d.ts +20 -0
- package/dist/system-prompt.d.ts +11 -0
- package/dist/tools.d.ts +21 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
CompactionEngine,
|
|
4
|
+
ManualCompactionError
|
|
5
|
+
} from "@deepseek-ai/dsh-compaction";
|
|
6
|
+
import { createCore } from "acp-kernel";
|
|
7
|
+
|
|
8
|
+
// src/state.ts
|
|
9
|
+
import { createInitialState } from "acp-kernel";
|
|
10
|
+
var AcpStateStore = class {
|
|
11
|
+
states = /* @__PURE__ */ new Map();
|
|
12
|
+
/** Kernel state for one session, initialised on first access. */
|
|
13
|
+
stateFor(session) {
|
|
14
|
+
const id = session.id;
|
|
15
|
+
const existing = this.states.get(id);
|
|
16
|
+
if (existing !== void 0) return existing;
|
|
17
|
+
const state = createInitialState();
|
|
18
|
+
this.states.set(id, state);
|
|
19
|
+
return state;
|
|
20
|
+
}
|
|
21
|
+
set(session, state) {
|
|
22
|
+
this.states.set(session.id, state);
|
|
23
|
+
}
|
|
24
|
+
delete(session) {
|
|
25
|
+
this.states.delete(session.id);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/tools.ts
|
|
30
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
31
|
+
import { estimateTokensFast } from "acp-kernel";
|
|
32
|
+
|
|
33
|
+
// src/config.ts
|
|
34
|
+
import { defaultConfig } from "acp-kernel";
|
|
35
|
+
function kernelConfigFor(input) {
|
|
36
|
+
const nudgePatch = {};
|
|
37
|
+
if (input.nudgeMinContextLimitPct !== void 0) nudgePatch.minContextLimitPct = input.nudgeMinContextLimitPct;
|
|
38
|
+
if (input.nudgeMaxContextLimitPct !== void 0) nudgePatch.maxContextLimitPct = input.nudgeMaxContextLimitPct;
|
|
39
|
+
if (input.nudgeEmergencyThresholdPct !== void 0) nudgePatch.emergencyThresholdPct = input.nudgeEmergencyThresholdPct;
|
|
40
|
+
const overrides = { ...input.coreOverrides };
|
|
41
|
+
if (Object.keys(nudgePatch).length > 0) {
|
|
42
|
+
overrides.nudge = { ...defaultConfig(input.modelContextLimit).nudge, ...nudgePatch };
|
|
43
|
+
}
|
|
44
|
+
return defaultConfig(input.modelContextLimit, overrides);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/region.ts
|
|
48
|
+
import { randomUUID } from "crypto";
|
|
49
|
+
import {
|
|
50
|
+
CompactionId,
|
|
51
|
+
compactCheckpointSource,
|
|
52
|
+
toolPairingBalancedAfter,
|
|
53
|
+
toolPairingBalancedBefore
|
|
54
|
+
} from "@deepseek-ai/dsh-compaction";
|
|
55
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
56
|
+
|
|
57
|
+
// src/messages.ts
|
|
58
|
+
function extractText(content) {
|
|
59
|
+
if (typeof content === "string") return content;
|
|
60
|
+
if (!Array.isArray(content)) return "";
|
|
61
|
+
const parts = [];
|
|
62
|
+
for (const block of content) {
|
|
63
|
+
if (block === null || typeof block !== "object") continue;
|
|
64
|
+
const b = block;
|
|
65
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
66
|
+
parts.push(b.text);
|
|
67
|
+
} else if (Array.isArray(b.content)) {
|
|
68
|
+
parts.push(extractText(b.content));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return parts.join("\n");
|
|
72
|
+
}
|
|
73
|
+
function toolCallsOf(content) {
|
|
74
|
+
if (!Array.isArray(content)) return [];
|
|
75
|
+
return content.filter((b) => b.type === "tool-call");
|
|
76
|
+
}
|
|
77
|
+
function stringifyArgs(args) {
|
|
78
|
+
if (!args) return "";
|
|
79
|
+
if (typeof args === "string") return args;
|
|
80
|
+
try {
|
|
81
|
+
return JSON.stringify(args);
|
|
82
|
+
} catch {
|
|
83
|
+
return String(args);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function projectEvent(event) {
|
|
87
|
+
switch (event.type) {
|
|
88
|
+
case "user/message": {
|
|
89
|
+
const text = extractText(event.data.content);
|
|
90
|
+
return text.length > 0 ? [{ id: String(event.seq), role: "user", contentType: "text", text }] : [];
|
|
91
|
+
}
|
|
92
|
+
case "assistant/message": {
|
|
93
|
+
const content = event.data.message?.content;
|
|
94
|
+
const calls = toolCallsOf(content);
|
|
95
|
+
const text = extractText(content);
|
|
96
|
+
if (calls.length === 0) {
|
|
97
|
+
return text.trim().length > 0 ? [{ id: String(event.seq), role: "assistant", contentType: "text", text }] : [];
|
|
98
|
+
}
|
|
99
|
+
if (calls.length === 1) {
|
|
100
|
+
const call = calls[0];
|
|
101
|
+
const argStr = stringifyArgs(call.arguments);
|
|
102
|
+
const body = argStr && text ? `${text}
|
|
103
|
+
${argStr}` : argStr || text;
|
|
104
|
+
return [{
|
|
105
|
+
id: String(event.seq),
|
|
106
|
+
role: "assistant",
|
|
107
|
+
contentType: "tool-call",
|
|
108
|
+
toolName: call.name ?? "",
|
|
109
|
+
toolCallId: call.id ?? "",
|
|
110
|
+
text: body
|
|
111
|
+
}];
|
|
112
|
+
}
|
|
113
|
+
return calls.map((call) => ({
|
|
114
|
+
id: `${event.seq}#${call.id ?? ""}`,
|
|
115
|
+
role: "assistant",
|
|
116
|
+
contentType: "tool-call",
|
|
117
|
+
toolName: call.name ?? "",
|
|
118
|
+
toolCallId: call.id ?? "",
|
|
119
|
+
text: stringifyArgs(call.arguments) || text
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
case "tool/result": {
|
|
123
|
+
const message = event.data.message;
|
|
124
|
+
const text = extractText(message?.content);
|
|
125
|
+
if (text.length === 0) return [];
|
|
126
|
+
return [{
|
|
127
|
+
id: String(event.seq),
|
|
128
|
+
role: "tool",
|
|
129
|
+
contentType: "tool-result",
|
|
130
|
+
toolName: message?.toolName ?? "",
|
|
131
|
+
toolCallId: message?.toolCallId ?? "",
|
|
132
|
+
text
|
|
133
|
+
}];
|
|
134
|
+
}
|
|
135
|
+
default:
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function eventsToCoreMessages(events) {
|
|
140
|
+
const out = [];
|
|
141
|
+
for (const event of events) out.push(...projectEvent(event));
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
function surfaceEventsOf(session) {
|
|
145
|
+
return session.surface.nodes.map((seq) => session.events[seq]).filter((event) => event !== void 0);
|
|
146
|
+
}
|
|
147
|
+
function extractEventText(event) {
|
|
148
|
+
switch (event.type) {
|
|
149
|
+
case "user/message":
|
|
150
|
+
return extractText(event.data.content);
|
|
151
|
+
case "assistant/message":
|
|
152
|
+
return extractText(event.data.message?.content);
|
|
153
|
+
case "tool/result":
|
|
154
|
+
return extractText(event.data.message?.content);
|
|
155
|
+
default:
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// src/region.ts
|
|
161
|
+
function findOpenTurn(events) {
|
|
162
|
+
let open = null;
|
|
163
|
+
for (const event of events) {
|
|
164
|
+
if (event.type === "turn/start") open = event.data.turn;
|
|
165
|
+
else if (event.type === "turn/end" && event.data.turn === open) open = null;
|
|
166
|
+
}
|
|
167
|
+
return open;
|
|
168
|
+
}
|
|
169
|
+
function assertNoActiveCompaction(events) {
|
|
170
|
+
let active = false;
|
|
171
|
+
for (const event of events) {
|
|
172
|
+
if (event.type === "compaction/start") active = true;
|
|
173
|
+
else if (event.type === "compaction/end") active = false;
|
|
174
|
+
}
|
|
175
|
+
if (active) {
|
|
176
|
+
throw new Error("billion-context-dsh: another compaction is already active for this session");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function resolveSurfaceRange(session, start, end) {
|
|
180
|
+
const nodes = session.surface.nodes;
|
|
181
|
+
let startIdx = nodes.indexOf(start);
|
|
182
|
+
let endIdx = nodes.indexOf(end);
|
|
183
|
+
if (startIdx < 0 || endIdx < 0) {
|
|
184
|
+
throw new Error(`billion-context-dsh: seq ${start}..${end} not in the current surface`);
|
|
185
|
+
}
|
|
186
|
+
if (startIdx > endIdx) {
|
|
187
|
+
throw new Error(`billion-context-dsh: reversed range ${start}..${end}`);
|
|
188
|
+
}
|
|
189
|
+
while (startIdx <= endIdx && !toolPairingBalancedBefore(session, nodes[startIdx])) {
|
|
190
|
+
startIdx += 1;
|
|
191
|
+
}
|
|
192
|
+
while (endIdx >= startIdx && !toolPairingBalancedAfter(session, nodes[endIdx])) {
|
|
193
|
+
endIdx -= 1;
|
|
194
|
+
}
|
|
195
|
+
if (startIdx > endIdx) {
|
|
196
|
+
throw new Error(
|
|
197
|
+
`billion-context-dsh: no tool-pairing-balanced range inside seq ${start}..${end} \u2014 narrow the range or consult acp_status for the current surface`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
return { start: nodes[startIdx], end: nodes[endIdx] };
|
|
201
|
+
}
|
|
202
|
+
function shadowedSeqsOf(session, start, end) {
|
|
203
|
+
const nodes = session.surface.nodes;
|
|
204
|
+
const startIdx = nodes.indexOf(start);
|
|
205
|
+
const endIdx = nodes.indexOf(end);
|
|
206
|
+
return nodes.slice(startIdx, endIdx + 1);
|
|
207
|
+
}
|
|
208
|
+
function runCompactionTransaction(session, input) {
|
|
209
|
+
assertNoActiveCompaction(session.events);
|
|
210
|
+
const turn = findOpenTurn(session.events);
|
|
211
|
+
const compactionId = CompactionId(randomUUID());
|
|
212
|
+
const seqs = [];
|
|
213
|
+
seqs.push(session.append("compaction/start", { compactionId, turn }).seq);
|
|
214
|
+
seqs.push(session.append("compaction/summary", {
|
|
215
|
+
compactionId,
|
|
216
|
+
summary: input.summary,
|
|
217
|
+
shadowedRange: { start: input.start, end: input.end },
|
|
218
|
+
shadowedSeqs: [...input.shadowedSeqs],
|
|
219
|
+
shadowedTokenCount: input.shadowedTokenCount,
|
|
220
|
+
provider: input.provider,
|
|
221
|
+
model: input.model
|
|
222
|
+
}).seq);
|
|
223
|
+
const message = createUserMessage({
|
|
224
|
+
content: input.summary,
|
|
225
|
+
source: compactCheckpointSource(compactionId)
|
|
226
|
+
});
|
|
227
|
+
seqs.push(session.append("user/message", message, {
|
|
228
|
+
surfaceOp: { op: "replace", start: input.start, end: input.end },
|
|
229
|
+
sourceEventSeqs: [...input.shadowedSeqs]
|
|
230
|
+
}).seq);
|
|
231
|
+
seqs.push(session.append("compaction/end", { compactionId, turn }).seq);
|
|
232
|
+
return { compactionId, seqs };
|
|
233
|
+
}
|
|
234
|
+
function rebuildBlockLedger(events) {
|
|
235
|
+
const ledger = [];
|
|
236
|
+
for (const event of events) {
|
|
237
|
+
if (event.type !== "compaction/summary") continue;
|
|
238
|
+
const data = event.data;
|
|
239
|
+
ledger.push({
|
|
240
|
+
blockId: data.compactionId,
|
|
241
|
+
summary: extractText(data.summary),
|
|
242
|
+
shadowedSeqs: [...data.shadowedSeqs],
|
|
243
|
+
shadowedTokenCount: data.shadowedTokenCount,
|
|
244
|
+
start: data.shadowedRange.start,
|
|
245
|
+
end: data.shadowedRange.end
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return ledger;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/tools.ts
|
|
252
|
+
function textOutput() {
|
|
253
|
+
return {
|
|
254
|
+
schema: {
|
|
255
|
+
type: "object",
|
|
256
|
+
properties: { text: { type: "string" } },
|
|
257
|
+
additionalProperties: false
|
|
258
|
+
},
|
|
259
|
+
render: (_args, value) => [{ type: "text", text: value.text }]
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function requireAgent(exec) {
|
|
263
|
+
if (exec.agent === void 0) {
|
|
264
|
+
throw new Error("billion-context-dsh: tool requires an agent execution context");
|
|
265
|
+
}
|
|
266
|
+
return exec.agent;
|
|
267
|
+
}
|
|
268
|
+
var compressParameters = {
|
|
269
|
+
topic: { type: "string", description: "Fallback topic for entries without their own." },
|
|
270
|
+
content: {
|
|
271
|
+
type: "array",
|
|
272
|
+
required: true,
|
|
273
|
+
description: "One or more ranges to compress, each with startSeq/endSeq boundaries (surface seqs) and a dense summary.",
|
|
274
|
+
items: {
|
|
275
|
+
type: "object",
|
|
276
|
+
properties: {
|
|
277
|
+
startSeq: {
|
|
278
|
+
oneOf: [
|
|
279
|
+
{ type: "integer", description: "First surface seq of the range." },
|
|
280
|
+
{ type: "string", description: "Seq as text; a trailing #callId fragment is ignored." }
|
|
281
|
+
]
|
|
282
|
+
},
|
|
283
|
+
endSeq: {
|
|
284
|
+
oneOf: [
|
|
285
|
+
{ type: "integer", description: "Inclusive last surface seq of the range." },
|
|
286
|
+
{ type: "string", description: "Seq as text; a trailing #callId fragment is ignored." }
|
|
287
|
+
]
|
|
288
|
+
},
|
|
289
|
+
summary: { type: "string", description: "Complete technical summary replacing the range; keep paths, decisions, values verbatim. Minimum 50 characters." },
|
|
290
|
+
topic: { type: "string", description: "Short label (3-5 words) for this range." }
|
|
291
|
+
},
|
|
292
|
+
additionalProperties: false
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
function parseSeq(value) {
|
|
297
|
+
const text = String(value).split("#")[0].trim();
|
|
298
|
+
const seq = Number(text);
|
|
299
|
+
if (!Number.isInteger(seq) || seq < 0) {
|
|
300
|
+
throw new Error(`billion-context-dsh: invalid seq "${String(value)}" \u2014 use a surface seq like 295`);
|
|
301
|
+
}
|
|
302
|
+
return seq;
|
|
303
|
+
}
|
|
304
|
+
async function handleCompress(env, args, exec) {
|
|
305
|
+
const agent = requireAgent(exec);
|
|
306
|
+
const session = agent.session;
|
|
307
|
+
const state = env.store.stateFor(session);
|
|
308
|
+
const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
|
|
309
|
+
const tokenCount = coreMessages.reduce((sum, message) => sum + estimateTokensFast(message.text ?? ""), 0);
|
|
310
|
+
const config = kernelConfigFor(env);
|
|
311
|
+
const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
|
|
312
|
+
env.store.set(session, turn.state);
|
|
313
|
+
const byRaw = turn.state.messageRefs.byRaw;
|
|
314
|
+
const ranges = args.content.map((range) => {
|
|
315
|
+
const startSeq = parseSeq(range.startSeq);
|
|
316
|
+
const endSeq = parseSeq(range.endSeq);
|
|
317
|
+
const startRef = byRaw[String(startSeq)];
|
|
318
|
+
const endRef = byRaw[String(endSeq)];
|
|
319
|
+
if (startRef === void 0 || endRef === void 0) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`billion-context-dsh: seq ${startSeq}..${endSeq} has no assigned ref \u2014 the range must be on the current surface (run acp_status for the live seq list)`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
startSeq,
|
|
326
|
+
endSeq,
|
|
327
|
+
startRef,
|
|
328
|
+
endRef,
|
|
329
|
+
summary: range.summary,
|
|
330
|
+
...(range.topic ?? args.topic) === void 0 ? {} : { topic: range.topic ?? args.topic }
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
const applied = env.kernel.applyCompression({
|
|
334
|
+
ranges: ranges.map(({ startRef, endRef, summary, topic }) => ({ startRef, endRef, summary, topic })),
|
|
335
|
+
messages: coreMessages,
|
|
336
|
+
state: turn.state,
|
|
337
|
+
config
|
|
338
|
+
});
|
|
339
|
+
if (applied.result.errors.length > 0) {
|
|
340
|
+
return { text: `compress failed: ${applied.result.errors.join("; ")}` };
|
|
341
|
+
}
|
|
342
|
+
env.store.set(session, applied.state);
|
|
343
|
+
const lines = [];
|
|
344
|
+
for (let index = 0; index < ranges.length; index += 1) {
|
|
345
|
+
const range = ranges[index];
|
|
346
|
+
const original = args.content[index];
|
|
347
|
+
const { start, end } = resolveSurfaceRange(session, range.startSeq, range.endSeq);
|
|
348
|
+
const shadowed = shadowedSeqsOf(session, start, end);
|
|
349
|
+
const { compactionId } = runCompactionTransaction(session, {
|
|
350
|
+
start,
|
|
351
|
+
end,
|
|
352
|
+
shadowedSeqs: shadowed,
|
|
353
|
+
summary: [{ type: "text", text: original.summary }],
|
|
354
|
+
shadowedTokenCount: 0,
|
|
355
|
+
provider: agent.options.provider ?? "",
|
|
356
|
+
model: agent.options.model ?? ""
|
|
357
|
+
});
|
|
358
|
+
const adjusted = start !== range.startSeq || end !== range.endSeq;
|
|
359
|
+
lines.push(
|
|
360
|
+
` block ${compactionId.slice(0, 8)}: seqs ${start}..${end}, ${shadowed.length} messages shadowed` + (adjusted ? ` (adjusted from ${range.startSeq}..${range.endSeq} to balanced edges)` : "")
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
text: `Compressed ${applied.result.blocksCreated} block(s), ~${applied.result.tokensCompressed} tokens reclaimed.
|
|
365
|
+
${lines.join("\n")}`
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
var decompressParameters = {
|
|
369
|
+
blockId: { type: "string", required: true, description: "Block id from acp_status or search_context (the compaction id)." }
|
|
370
|
+
};
|
|
371
|
+
function handleDecompress(_env, args, exec) {
|
|
372
|
+
const session = requireAgent(exec).session;
|
|
373
|
+
const ledger = rebuildBlockLedger(session.events);
|
|
374
|
+
const block = ledger.find((entry) => entry.blockId.startsWith(args.blockId));
|
|
375
|
+
if (block === void 0) {
|
|
376
|
+
return { text: `decompress: block "${args.blockId}" not found (see acp_status for the block list)` };
|
|
377
|
+
}
|
|
378
|
+
const parts = [];
|
|
379
|
+
for (const seq of block.shadowedSeqs) {
|
|
380
|
+
const event = session.events[seq];
|
|
381
|
+
const text = event === void 0 ? "" : extractEventText(event);
|
|
382
|
+
if (text.length > 0) parts.push(`[seq ${seq}] ${text}`);
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
text: `Block ${block.blockId} \u2014 ${block.summary}
|
|
386
|
+
|
|
387
|
+
${parts.join("\n\n") || "(no recoverable content)"}`
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
var searchParameters = {
|
|
391
|
+
query: { type: "string", required: true, description: "Search terms to find inside compressed blocks." },
|
|
392
|
+
limit: { type: "integer", description: "Maximum results (default 5)." }
|
|
393
|
+
};
|
|
394
|
+
function handleSearch(_env, args, exec) {
|
|
395
|
+
const session = requireAgent(exec).session;
|
|
396
|
+
const ledger = rebuildBlockLedger(session.events);
|
|
397
|
+
const terms = args.query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
398
|
+
const scored = [];
|
|
399
|
+
for (const block of ledger) {
|
|
400
|
+
const original = block.shadowedSeqs.map((seq) => extractEventText(session.events[seq])).join("\n");
|
|
401
|
+
const haystack = `${block.summary}
|
|
402
|
+
${original}`.toLowerCase();
|
|
403
|
+
let score = 0;
|
|
404
|
+
for (const term of terms) score += haystack.split(term).length - 1;
|
|
405
|
+
if (score > 0) scored.push({ blockId: block.blockId, score, summary: block.summary });
|
|
406
|
+
}
|
|
407
|
+
scored.sort((a, b) => b.score - a.score);
|
|
408
|
+
const top = scored.slice(0, args.limit ?? 5);
|
|
409
|
+
if (top.length === 0) return { text: `search_context: no matches for "${args.query}"` };
|
|
410
|
+
return {
|
|
411
|
+
text: `Matches for "${args.query}":
|
|
412
|
+
` + top.map((hit) => ` - ${hit.blockId} (score ${hit.score}): ${hit.summary.slice(0, 160)}`).join("\n") + "\n\nDecompress with: decompress({ blockId })"
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
var statusParameters = {};
|
|
416
|
+
function handleStatus(env, _args, exec) {
|
|
417
|
+
const session = requireAgent(exec).session;
|
|
418
|
+
const ledger = rebuildBlockLedger(session.events);
|
|
419
|
+
const totalTokens = ledger.reduce((sum, block) => sum + block.shadowedTokenCount, 0);
|
|
420
|
+
const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
|
|
421
|
+
const estimated = coreMessages.reduce((sum, message) => sum + estimateTokensFast(message.text ?? ""), 0);
|
|
422
|
+
const limit = env.modelContextLimit;
|
|
423
|
+
const lines = [
|
|
424
|
+
`ACP status \u2014 session ${session.id}`,
|
|
425
|
+
` blocks: ${ledger.length}`,
|
|
426
|
+
` tokens compressed: ${totalTokens}`,
|
|
427
|
+
` estimated context: ${estimated} / ${limit} (${Math.round(estimated / limit * 100)}%)`
|
|
428
|
+
];
|
|
429
|
+
for (const block of ledger.slice(0, 10)) {
|
|
430
|
+
lines.push(` - ${block.blockId.slice(0, 8)}: seqs ${block.start}..${block.end} (${block.shadowedSeqs.length} msgs) \u2014 ${block.summary.slice(0, 80)}`);
|
|
431
|
+
}
|
|
432
|
+
return { text: lines.join("\n") };
|
|
433
|
+
}
|
|
434
|
+
function makeTools(env) {
|
|
435
|
+
return [
|
|
436
|
+
defineTool({
|
|
437
|
+
name: "compress",
|
|
438
|
+
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. Never compress content the current step is actively using.",
|
|
439
|
+
parameters: compressParameters,
|
|
440
|
+
output: textOutput(),
|
|
441
|
+
async execute(args, exec) {
|
|
442
|
+
return handleCompress(env, args, exec);
|
|
443
|
+
}
|
|
444
|
+
}),
|
|
445
|
+
defineTool({
|
|
446
|
+
name: "decompress",
|
|
447
|
+
description: "Recover the original content of a compressed block by its blockId (read-only; does not unshadow the range).",
|
|
448
|
+
parameters: decompressParameters,
|
|
449
|
+
output: textOutput(),
|
|
450
|
+
execute(args, exec) {
|
|
451
|
+
return Promise.resolve(handleDecompress(env, args, exec));
|
|
452
|
+
}
|
|
453
|
+
}),
|
|
454
|
+
defineTool({
|
|
455
|
+
name: "search_context",
|
|
456
|
+
description: "Search inside compressed blocks (summaries and original content) for information the model no longer sees in context.",
|
|
457
|
+
parameters: searchParameters,
|
|
458
|
+
output: textOutput(),
|
|
459
|
+
execute(args, exec) {
|
|
460
|
+
return Promise.resolve(handleSearch(env, args, exec));
|
|
461
|
+
}
|
|
462
|
+
}),
|
|
463
|
+
defineTool({
|
|
464
|
+
name: "acp_status",
|
|
465
|
+
description: "Report the ACP block ledger: compressed blocks, reclaimed tokens, and current context pressure.",
|
|
466
|
+
parameters: statusParameters,
|
|
467
|
+
output: textOutput(),
|
|
468
|
+
execute(args, exec) {
|
|
469
|
+
return Promise.resolve(handleStatus(env, args, exec));
|
|
470
|
+
}
|
|
471
|
+
})
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/commands.ts
|
|
476
|
+
import { estimateTokensFast as estimateTokensFast2 } from "acp-kernel";
|
|
477
|
+
function statusText(env, agent) {
|
|
478
|
+
const session = agent.session;
|
|
479
|
+
const ledger = rebuildBlockLedger(session.events);
|
|
480
|
+
const totalTokens = ledger.reduce((sum, block) => sum + block.shadowedTokenCount, 0);
|
|
481
|
+
const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
|
|
482
|
+
const estimated = coreMessages.reduce((sum, message) => sum + estimateTokensFast2(message.text ?? ""), 0);
|
|
483
|
+
const limit = env.modelContextLimit;
|
|
484
|
+
const lines = [
|
|
485
|
+
`ACP status \u2014 session ${session.id}`,
|
|
486
|
+
` blocks: ${ledger.length}`,
|
|
487
|
+
` tokens compressed: ${totalTokens}`,
|
|
488
|
+
` estimated context: ${estimated} / ${limit} (${Math.round(estimated / limit * 100)}%)`
|
|
489
|
+
];
|
|
490
|
+
for (const block of ledger.slice(0, 10)) {
|
|
491
|
+
lines.push(` - ${block.blockId.slice(0, 8)}: seqs ${block.start}..${block.end} \u2014 ${block.summary.slice(0, 80)}`);
|
|
492
|
+
}
|
|
493
|
+
return lines.join("\n");
|
|
494
|
+
}
|
|
495
|
+
function compressText(env, agent, args) {
|
|
496
|
+
if (args.length < 3) {
|
|
497
|
+
return "/acp compress <startSeq> <endSeq> <summary...>";
|
|
498
|
+
}
|
|
499
|
+
const startSeq = Number(args[0]);
|
|
500
|
+
const endSeq = Number(args[1]);
|
|
501
|
+
const summary = args.slice(2).join(" ");
|
|
502
|
+
if (!Number.isInteger(startSeq) || !Number.isInteger(endSeq)) {
|
|
503
|
+
return "/acp compress: startSeq and endSeq must be integers";
|
|
504
|
+
}
|
|
505
|
+
const session = agent.session;
|
|
506
|
+
const { start, end } = resolveSurfaceRange(session, startSeq, endSeq);
|
|
507
|
+
const shadowed = shadowedSeqsOf(session, startSeq, endSeq);
|
|
508
|
+
const { compactionId } = runCompactionTransaction(session, {
|
|
509
|
+
start,
|
|
510
|
+
end,
|
|
511
|
+
shadowedSeqs: shadowed,
|
|
512
|
+
summary: [{ type: "text", text: summary }],
|
|
513
|
+
shadowedTokenCount: 0,
|
|
514
|
+
provider: agent.options.provider ?? "",
|
|
515
|
+
model: agent.options.model ?? ""
|
|
516
|
+
});
|
|
517
|
+
return `Compressed seqs ${start}..${end} (${shadowed.length} messages) as block ${compactionId.slice(0, 8)}`;
|
|
518
|
+
}
|
|
519
|
+
function decompressText(_env, agent, args) {
|
|
520
|
+
if (args.length < 1) return "/acp decompress <blockId>";
|
|
521
|
+
const session = agent.session;
|
|
522
|
+
const ledger = rebuildBlockLedger(session.events);
|
|
523
|
+
const block = ledger.find((entry) => entry.blockId.startsWith(args[0]));
|
|
524
|
+
if (block === void 0) return `block "${args[0]}" not found (see /acp status)`;
|
|
525
|
+
const parts = block.shadowedSeqs.map((seq) => extractEventText(session.events[seq])).filter((text) => text.length > 0);
|
|
526
|
+
return `Block ${block.blockId} \u2014 ${block.summary}
|
|
527
|
+
|
|
528
|
+
${parts.join("\n\n") || "(no recoverable content)"}`;
|
|
529
|
+
}
|
|
530
|
+
function acpCommand(env) {
|
|
531
|
+
return {
|
|
532
|
+
name: "acp",
|
|
533
|
+
description: "Active Context Pruning \u2014 model-driven context compression. Usage: /acp status | /acp compress <startSeq> <endSeq> <summary> | /acp decompress <blockId>",
|
|
534
|
+
handler: async (invocation) => {
|
|
535
|
+
const raw = invocation.rawInput.trim();
|
|
536
|
+
if (raw === "" || raw === "status") {
|
|
537
|
+
return { kind: "success", text: statusText(env, invocation.agent) };
|
|
538
|
+
}
|
|
539
|
+
if (raw.startsWith("compress")) {
|
|
540
|
+
return { kind: "success", text: compressText(env, invocation.agent, raw.slice("compress".length).trim().split(/\s+/)) };
|
|
541
|
+
}
|
|
542
|
+
if (raw.startsWith("decompress")) {
|
|
543
|
+
return { kind: "success", text: decompressText(env, invocation.agent, raw.slice("decompress".length).trim().split(/\s+/)) };
|
|
544
|
+
}
|
|
545
|
+
return { kind: "error", text: `unknown /acp subcommand "${raw.split(/\s+/)[0]}" \u2014 use status | compress | decompress` };
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/nudge.ts
|
|
551
|
+
import {
|
|
552
|
+
estimateTokensFast as estimateTokensFast3
|
|
553
|
+
} from "acp-kernel";
|
|
554
|
+
import { createUserMessage as createUserMessage2 } from "@deepseek-ai/dsh-llm";
|
|
555
|
+
function rangeTable(nudge, state) {
|
|
556
|
+
const byRef = state.messageRefs.byRef;
|
|
557
|
+
const lines = nudge.compressibleRanges.slice(0, 6).map((range) => {
|
|
558
|
+
const startRaw = byRef[range.startRef];
|
|
559
|
+
const endRaw = byRef[range.endRef];
|
|
560
|
+
if (startRaw === void 0 || endRaw === void 0) return null;
|
|
561
|
+
return ` - seq ${startRaw}..${endRaw} \u2014 ${range.count} messages, ~${range.tokens} tokens`;
|
|
562
|
+
});
|
|
563
|
+
const visible = lines.filter((line) => line !== null);
|
|
564
|
+
if (visible.length === 0) return "";
|
|
565
|
+
return [
|
|
566
|
+
"",
|
|
567
|
+
"Compressible ranges (refs are surface seqs):",
|
|
568
|
+
...visible,
|
|
569
|
+
"Compress with: compress({ content: [{ startSeq, endSeq, summary }] })"
|
|
570
|
+
].join("\n");
|
|
571
|
+
}
|
|
572
|
+
function buildNudge(agent, env, lastNudgeTurn) {
|
|
573
|
+
const session = agent.session;
|
|
574
|
+
const state = env.store.stateFor(session);
|
|
575
|
+
const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
|
|
576
|
+
const tokenCount = coreMessages.reduce((sum, message2) => sum + estimateTokensFast3(message2.text ?? ""), 0);
|
|
577
|
+
const config = kernelConfigFor(env);
|
|
578
|
+
const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
|
|
579
|
+
env.store.set(session, turn.state);
|
|
580
|
+
const nudge = turn.nudge;
|
|
581
|
+
if (nudge === void 0 || !nudge.shouldInject) return null;
|
|
582
|
+
const emergency = nudge.breakdown?.emergencyOverride === 1;
|
|
583
|
+
const turnNumber = findOpenTurn(session.events) ?? 0;
|
|
584
|
+
const alreadyShown = !emergency && lastNudgeTurn.get(session.id) === turnNumber;
|
|
585
|
+
if (alreadyShown) return null;
|
|
586
|
+
lastNudgeTurn.set(session.id, turnNumber);
|
|
587
|
+
const text = buildNudgeText(nudge, emergency, turn.state);
|
|
588
|
+
const message = createUserMessage2({
|
|
589
|
+
content: [{ type: "text", text }],
|
|
590
|
+
source: { kind: "plugin", plugin: "acp-nudge" }
|
|
591
|
+
});
|
|
592
|
+
return { message, emergency };
|
|
593
|
+
}
|
|
594
|
+
function buildNudgeText(nudge, emergency, state) {
|
|
595
|
+
const pct = Math.round(nudge.contextUsage * 100);
|
|
596
|
+
const frame = emergency ? `\u26A0\uFE0F Context usage is at ${pct}% of the window \u2014 nearly full. Consider compressing consumed ranges soon so working context stays available; the choice and timing are yours.` : `Context usage is at ${pct}%. This is a suggestion, not a requirement \u2014 you decide whether and when to compress.`;
|
|
597
|
+
const guidance = "Compress by need, not by percentage: replace only ranges you have genuinely consumed, with dense self-contained summaries.";
|
|
598
|
+
return [frame, "", guidance, rangeTable(nudge, state)].join("\n");
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/system-prompt.ts
|
|
602
|
+
import { COMPRESS_PHILOSOPHY } from "acp-kernel";
|
|
603
|
+
var ACP_SYSTEM_PROMPT = `Active Context Pruning \u2014 model-driven context management
|
|
604
|
+
|
|
605
|
+
YOU decide whether and when to compress context. Nothing forces you: the injected "nudge" is a suggestion, not an order, and you may ignore it when compression would not help. Compress only ranges you have genuinely consumed (read tool outputs, finished explorations, superseded steps) that the current work no longer needs verbatim.
|
|
606
|
+
|
|
607
|
+
${COMPRESS_PHILOSOPHY}
|
|
608
|
+
|
|
609
|
+
Compression tools (refs are SURFACE SEQS, not ids):
|
|
610
|
+
- compress: replace a seq range with your dense self-contained summary. compress({ content: [{ startSeq, endSeq, summary }] }). Edges are auto-balanced to tool-call/result boundaries; a trailing #callId fragment in a seq is ignored. Ranges must be on the current surface \u2014 stale seqs fail with guidance.
|
|
611
|
+
- decompress: recover a compressed block's original content, read-only. decompress({ blockId }).
|
|
612
|
+
- search_context: find information inside compressed blocks BEFORE decompressing. search_context({ query }).
|
|
613
|
+
- acp_status: current context usage and the live compressible-range list. Run it before compressing when in doubt.
|
|
614
|
+
|
|
615
|
+
When you write a summary, it becomes the ONLY record of that range: keep file paths, signatures, exact values, decisions, and error strings verbatim so a later reader (or you, after decompress) can continue without the original. Never reuse historical seqs \u2014 the surface moves as messages land and compress; verify with acp_status.`;
|
|
616
|
+
var ACP_SYSTEM_PROMPT_ORDER = 150;
|
|
617
|
+
|
|
618
|
+
// src/index.ts
|
|
619
|
+
var DEFAULT_CONFIG = {
|
|
620
|
+
modelContextLimit: 128e3,
|
|
621
|
+
autoTools: true,
|
|
622
|
+
autoCommand: true,
|
|
623
|
+
autoNudge: true
|
|
624
|
+
};
|
|
625
|
+
function resolveAcpConfig(config = {}) {
|
|
626
|
+
return { ...DEFAULT_CONFIG, ...config };
|
|
627
|
+
}
|
|
628
|
+
var AcpCompactionEngine = class extends CompactionEngine {
|
|
629
|
+
/** The framework-agnostic ACP compression core, reused verbatim. */
|
|
630
|
+
kernel;
|
|
631
|
+
/** Per-session kernel state. */
|
|
632
|
+
store;
|
|
633
|
+
/** Resolved engine configuration. */
|
|
634
|
+
config;
|
|
635
|
+
lastNudgeTurn = /* @__PURE__ */ new Map();
|
|
636
|
+
constructor(ctx, config = {}) {
|
|
637
|
+
super(ctx);
|
|
638
|
+
this.config = resolveAcpConfig(config);
|
|
639
|
+
this.kernel = createCore({});
|
|
640
|
+
this.store = new AcpStateStore();
|
|
641
|
+
const env = {
|
|
642
|
+
kernel: this.kernel,
|
|
643
|
+
store: this.store,
|
|
644
|
+
modelContextLimit: this.config.modelContextLimit,
|
|
645
|
+
nudgeMinContextLimitPct: this.config.nudgeMinContextLimitPct,
|
|
646
|
+
nudgeMaxContextLimitPct: this.config.nudgeMaxContextLimitPct,
|
|
647
|
+
nudgeEmergencyThresholdPct: this.config.nudgeEmergencyThresholdPct,
|
|
648
|
+
coreOverrides: this.config.coreOverrides
|
|
649
|
+
};
|
|
650
|
+
if (this.config.autoTools) {
|
|
651
|
+
const tools = ctx.get("tools");
|
|
652
|
+
if (tools !== void 0) {
|
|
653
|
+
for (const tool of makeTools(env)) tools.register(tool);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (this.config.autoCommand) {
|
|
657
|
+
const commands = ctx.get("commands");
|
|
658
|
+
if (commands !== void 0) commands.register(acpCommand(env));
|
|
659
|
+
}
|
|
660
|
+
if (this.config.autoNudge) {
|
|
661
|
+
ctx.on("agent/pre-step", async (payload, next) => {
|
|
662
|
+
const decision = await next();
|
|
663
|
+
if (decision.kind === "reject") return decision;
|
|
664
|
+
const outcome = buildNudge(payload.agent, env, this.lastNudgeTurn);
|
|
665
|
+
if (outcome === null) return decision;
|
|
666
|
+
return { kind: "enter", messages: [...decision.messages, outcome.message] };
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
const systemPrompt = ctx.get("systemPrompt");
|
|
670
|
+
if (systemPrompt !== void 0) {
|
|
671
|
+
systemPrompt.section({
|
|
672
|
+
name: "billion-context-dsh",
|
|
673
|
+
order: ACP_SYSTEM_PROMPT_ORDER,
|
|
674
|
+
text: ACP_SYSTEM_PROMPT
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
/** ACP is model-driven: automatic pressure policy never summarizes by itself. */
|
|
679
|
+
async compactIfNeeded(_agent, _trigger, signal) {
|
|
680
|
+
signal.throwIfAborted();
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
/** Explicit idle-session compaction: ACP leaves the decision to the model. */
|
|
684
|
+
async compactNow(_agent, signal) {
|
|
685
|
+
signal.throwIfAborted();
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* The model-driven path lands through the `compress` tool, which runs the
|
|
690
|
+
* full durable transaction directly. This seam method rejects with guidance:
|
|
691
|
+
* automatic summarization is exactly what ACP replaces.
|
|
692
|
+
*/
|
|
693
|
+
async compactRegion(_start, _end, _agent, signal) {
|
|
694
|
+
signal?.throwIfAborted();
|
|
695
|
+
throw new ManualCompactionError(
|
|
696
|
+
"summary",
|
|
697
|
+
"billion-context-dsh is model-driven: use the compress tool instead of automatic summarization"
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
var index_default = AcpCompactionEngine;
|
|
702
|
+
export {
|
|
703
|
+
ACP_SYSTEM_PROMPT,
|
|
704
|
+
ACP_SYSTEM_PROMPT_ORDER,
|
|
705
|
+
AcpCompactionEngine,
|
|
706
|
+
AcpStateStore,
|
|
707
|
+
acpCommand,
|
|
708
|
+
assertNoActiveCompaction,
|
|
709
|
+
buildNudge,
|
|
710
|
+
index_default as default,
|
|
711
|
+
eventsToCoreMessages,
|
|
712
|
+
extractEventText,
|
|
713
|
+
findOpenTurn,
|
|
714
|
+
kernelConfigFor,
|
|
715
|
+
makeTools,
|
|
716
|
+
projectEvent,
|
|
717
|
+
rebuildBlockLedger,
|
|
718
|
+
resolveAcpConfig,
|
|
719
|
+
resolveSurfaceRange,
|
|
720
|
+
runCompactionTransaction,
|
|
721
|
+
shadowedSeqsOf,
|
|
722
|
+
surfaceEventsOf
|
|
723
|
+
};
|
|
724
|
+
//# sourceMappingURL=index.js.map
|