billion-context-dsh 0.1.0 → 0.1.2

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/dist/index.js CHANGED
@@ -28,7 +28,7 @@ var AcpStateStore = class {
28
28
 
29
29
  // src/tools.ts
30
30
  import { defineTool } from "@deepseek-ai/dsh-tools";
31
- import { estimateTokensFast } from "acp-kernel";
31
+ import { defaultCountTokens as defaultCountTokens2 } from "acp-kernel";
32
32
 
33
33
  // src/config.ts
34
34
  import { defaultConfig } from "acp-kernel";
@@ -53,6 +53,7 @@ import {
53
53
  toolPairingBalancedBefore
54
54
  } from "@deepseek-ai/dsh-compaction";
55
55
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
56
+ import { defaultCountTokens } from "acp-kernel";
56
57
 
57
58
  // src/messages.ts
58
59
  function extractText(content) {
@@ -176,28 +177,62 @@ function assertNoActiveCompaction(events) {
176
177
  throw new Error("billion-context-dsh: another compaction is already active for this session");
177
178
  }
178
179
  }
180
+ function hasPlainRef(session, seq) {
181
+ const event = session.events[seq];
182
+ if (event === void 0) return false;
183
+ switch (event.type) {
184
+ case "user/message":
185
+ case "tool/result":
186
+ return extractEventText(event).trim().length > 0;
187
+ case "assistant/message": {
188
+ const content = event.data.message?.content;
189
+ const calls = Array.isArray(content) ? content.filter(
190
+ (block) => block !== null && typeof block === "object" && block.type === "tool-call"
191
+ ) : [];
192
+ if (calls.length > 1) return false;
193
+ return calls.length === 1 || extractEventText(event).trim().length > 0;
194
+ }
195
+ default:
196
+ return false;
197
+ }
198
+ }
179
199
  function resolveSurfaceRange(session, start, end) {
180
200
  const nodes = session.surface.nodes;
181
- let startIdx = nodes.indexOf(start);
182
- let endIdx = nodes.indexOf(end);
183
- if (startIdx < 0 || endIdx < 0) {
201
+ const requestedStartIdx = nodes.indexOf(start);
202
+ const requestedEndIdx = nodes.indexOf(end);
203
+ if (requestedStartIdx < 0 || requestedEndIdx < 0) {
184
204
  throw new Error(`billion-context-dsh: seq ${start}..${end} not in the current surface`);
185
205
  }
186
- if (startIdx > endIdx) {
206
+ if (requestedStartIdx > requestedEndIdx) {
187
207
  throw new Error(`billion-context-dsh: reversed range ${start}..${end}`);
188
208
  }
189
- while (startIdx <= endIdx && !toolPairingBalancedBefore(session, nodes[startIdx])) {
209
+ const cleanBefore = (index) => toolPairingBalancedBefore(session, nodes[index]) && hasPlainRef(session, nodes[index]);
210
+ const cleanAfter = (index) => toolPairingBalancedAfter(session, nodes[index]) && hasPlainRef(session, nodes[index]);
211
+ let startIdx = requestedStartIdx;
212
+ let endIdx = requestedEndIdx;
213
+ while (startIdx <= endIdx && !cleanBefore(startIdx)) {
190
214
  startIdx += 1;
191
215
  }
192
- while (endIdx >= startIdx && !toolPairingBalancedAfter(session, nodes[endIdx])) {
216
+ while (endIdx >= startIdx && !cleanAfter(endIdx)) {
193
217
  endIdx -= 1;
194
218
  }
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
- );
219
+ if (startIdx <= endIdx) {
220
+ return { start: nodes[startIdx], end: nodes[endIdx] };
221
+ }
222
+ startIdx = requestedStartIdx;
223
+ endIdx = requestedEndIdx;
224
+ while (startIdx > 0 && !cleanBefore(startIdx)) {
225
+ startIdx -= 1;
226
+ }
227
+ while (endIdx < nodes.length - 1 && !cleanAfter(endIdx)) {
228
+ endIdx += 1;
199
229
  }
200
- return { start: nodes[startIdx], end: nodes[endIdx] };
230
+ if (cleanBefore(startIdx) && cleanAfter(endIdx)) {
231
+ return { start: nodes[startIdx], end: nodes[endIdx] };
232
+ }
233
+ throw new Error(
234
+ `billion-context-dsh: no tool-pairing-balanced range around seq ${start}..${end} \u2014 narrow the range or consult acp_status for the current surface`
235
+ );
201
236
  }
202
237
  function shadowedSeqsOf(session, start, end) {
203
238
  const nodes = session.surface.nodes;
@@ -236,17 +271,73 @@ function rebuildBlockLedger(events) {
236
271
  for (const event of events) {
237
272
  if (event.type !== "compaction/summary") continue;
238
273
  const data = event.data;
274
+ let shadowedTokenCount = data.shadowedTokenCount;
275
+ if (shadowedTokenCount === 0) {
276
+ shadowedTokenCount = 0;
277
+ for (const seq of data.shadowedSeqs) {
278
+ const original = events[seq];
279
+ if (original !== void 0) shadowedTokenCount += defaultCountTokens(extractEventText(original));
280
+ }
281
+ }
239
282
  ledger.push({
240
283
  blockId: data.compactionId,
241
284
  summary: extractText(data.summary),
242
285
  shadowedSeqs: [...data.shadowedSeqs],
243
- shadowedTokenCount: data.shadowedTokenCount,
286
+ shadowedTokenCount,
244
287
  start: data.shadowedRange.start,
245
288
  end: data.shadowedRange.end
246
289
  });
247
290
  }
248
291
  return ledger;
249
292
  }
293
+ function isCheckpointNode(event) {
294
+ if (event.type !== "user/message") return false;
295
+ const source = event.data.source;
296
+ return source?.plugin === "compact";
297
+ }
298
+ function buildCompressibleSeqRanges(session, opts = {}) {
299
+ const nodes = session.surface.nodes;
300
+ const preserve = opts.preserveRecent ?? 5;
301
+ const protectedSeqs = /* @__PURE__ */ new Set();
302
+ for (const seq of nodes.slice(-preserve)) protectedSeqs.add(seq);
303
+ for (let index = nodes.length - 1; index >= 0; index -= 1) {
304
+ const event = session.events[nodes[index]];
305
+ if (event?.type === "user/message" && !isCheckpointNode(event)) {
306
+ protectedSeqs.add(nodes[index]);
307
+ break;
308
+ }
309
+ }
310
+ const raw = [];
311
+ let cur = null;
312
+ const flush = () => {
313
+ if (cur !== null) raw.push(cur);
314
+ cur = null;
315
+ };
316
+ for (const seq of nodes) {
317
+ const event = session.events[seq];
318
+ if (event === void 0 || protectedSeqs.has(seq) || isCheckpointNode(event)) {
319
+ flush();
320
+ continue;
321
+ }
322
+ const tokens = defaultCountTokens(extractEventText(event));
323
+ if (cur === null) {
324
+ cur = { start: seq, end: seq, count: 1, tokens };
325
+ } else {
326
+ cur = { start: cur.start, end: seq, count: cur.count + 1, tokens: cur.tokens + tokens };
327
+ }
328
+ }
329
+ flush();
330
+ const out = [];
331
+ for (const range of raw) {
332
+ try {
333
+ const { start, end } = resolveSurfaceRange(session, range.start, range.end);
334
+ const count = range.count;
335
+ out.push({ start, end, count, tokens: range.tokens });
336
+ } catch {
337
+ }
338
+ }
339
+ return out.sort((a, b) => b.tokens - a.tokens);
340
+ }
250
341
 
251
342
  // src/tools.ts
252
343
  function textOutput() {
@@ -306,7 +397,7 @@ async function handleCompress(env, args, exec) {
306
397
  const session = agent.session;
307
398
  const state = env.store.stateFor(session);
308
399
  const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
309
- const tokenCount = coreMessages.reduce((sum, message) => sum + estimateTokensFast(message.text ?? ""), 0);
400
+ const tokenCount = coreMessages.reduce((sum, message) => sum + defaultCountTokens2(message.text ?? ""), 0);
310
401
  const config = kernelConfigFor(env);
311
402
  const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
312
403
  env.store.set(session, turn.state);
@@ -314,16 +405,19 @@ async function handleCompress(env, args, exec) {
314
405
  const ranges = args.content.map((range) => {
315
406
  const startSeq = parseSeq(range.startSeq);
316
407
  const endSeq = parseSeq(range.endSeq);
317
- const startRef = byRaw[String(startSeq)];
318
- const endRef = byRaw[String(endSeq)];
408
+ const { start, end } = resolveSurfaceRange(session, startSeq, endSeq);
409
+ const startRef = byRaw[String(start)];
410
+ const endRef = byRaw[String(end)];
319
411
  if (startRef === void 0 || endRef === void 0) {
320
412
  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)`
413
+ `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)`
322
414
  );
323
415
  }
324
416
  return {
325
417
  startSeq,
326
418
  endSeq,
419
+ start,
420
+ end,
327
421
  startRef,
328
422
  endRef,
329
423
  summary: range.summary,
@@ -344,14 +438,19 @@ async function handleCompress(env, args, exec) {
344
438
  for (let index = 0; index < ranges.length; index += 1) {
345
439
  const range = ranges[index];
346
440
  const original = args.content[index];
347
- const { start, end } = resolveSurfaceRange(session, range.startSeq, range.endSeq);
441
+ const { start, end } = range;
348
442
  const shadowed = shadowedSeqsOf(session, start, end);
443
+ let shadowedTokens = 0;
444
+ for (const seq of shadowed) {
445
+ const event = session.events[seq];
446
+ if (event !== void 0) shadowedTokens += defaultCountTokens2(extractEventText(event));
447
+ }
349
448
  const { compactionId } = runCompactionTransaction(session, {
350
449
  start,
351
450
  end,
352
451
  shadowedSeqs: shadowed,
353
452
  summary: [{ type: "text", text: original.summary }],
354
- shadowedTokenCount: 0,
453
+ shadowedTokenCount: shadowedTokens,
355
454
  provider: agent.options.provider ?? "",
356
455
  model: agent.options.model ?? ""
357
456
  });
@@ -418,7 +517,7 @@ function handleStatus(env, _args, exec) {
418
517
  const ledger = rebuildBlockLedger(session.events);
419
518
  const totalTokens = ledger.reduce((sum, block) => sum + block.shadowedTokenCount, 0);
420
519
  const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
421
- const estimated = coreMessages.reduce((sum, message) => sum + estimateTokensFast(message.text ?? ""), 0);
520
+ const estimated = coreMessages.reduce((sum, message) => sum + defaultCountTokens2(message.text ?? ""), 0);
422
521
  const limit = env.modelContextLimit;
423
522
  const lines = [
424
523
  `ACP status \u2014 session ${session.id}`,
@@ -473,13 +572,13 @@ function makeTools(env) {
473
572
  }
474
573
 
475
574
  // src/commands.ts
476
- import { estimateTokensFast as estimateTokensFast2 } from "acp-kernel";
575
+ import { defaultCountTokens as defaultCountTokens3 } from "acp-kernel";
477
576
  function statusText(env, agent) {
478
577
  const session = agent.session;
479
578
  const ledger = rebuildBlockLedger(session.events);
480
579
  const totalTokens = ledger.reduce((sum, block) => sum + block.shadowedTokenCount, 0);
481
580
  const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
482
- const estimated = coreMessages.reduce((sum, message) => sum + estimateTokensFast2(message.text ?? ""), 0);
581
+ const estimated = coreMessages.reduce((sum, message) => sum + defaultCountTokens3(message.text ?? ""), 0);
483
582
  const limit = env.modelContextLimit;
484
583
  const lines = [
485
584
  `ACP status \u2014 session ${session.id}`,
@@ -505,12 +604,17 @@ function compressText(env, agent, args) {
505
604
  const session = agent.session;
506
605
  const { start, end } = resolveSurfaceRange(session, startSeq, endSeq);
507
606
  const shadowed = shadowedSeqsOf(session, startSeq, endSeq);
607
+ let shadowedTokens = 0;
608
+ for (const seq of shadowed) {
609
+ const event = session.events[seq];
610
+ if (event !== void 0) shadowedTokens += defaultCountTokens3(extractEventText(event));
611
+ }
508
612
  const { compactionId } = runCompactionTransaction(session, {
509
613
  start,
510
614
  end,
511
615
  shadowedSeqs: shadowed,
512
616
  summary: [{ type: "text", text: summary }],
513
- shadowedTokenCount: 0,
617
+ shadowedTokenCount: shadowedTokens,
514
618
  provider: agent.options.provider ?? "",
515
619
  model: agent.options.model ?? ""
516
620
  });
@@ -549,31 +653,31 @@ function acpCommand(env) {
549
653
 
550
654
  // src/nudge.ts
551
655
  import {
552
- estimateTokensFast as estimateTokensFast3
656
+ defaultCountTokens as defaultCountTokens4
553
657
  } from "acp-kernel";
554
658
  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 "";
659
+ function rangeTable(session) {
660
+ const ranges = buildCompressibleSeqRanges(session).slice(0, 6);
661
+ if (ranges.length === 0) return "";
662
+ const lines = ranges.map((range) => ` - seq ${range.start}..${range.end} \u2014 ${range.count} messages, ~${range.tokens} tokens`);
565
663
  return [
566
664
  "",
567
665
  "Compressible ranges (refs are surface seqs):",
568
- ...visible,
666
+ ...lines,
569
667
  "Compress with: compress({ content: [{ startSeq, endSeq, summary }] })"
570
668
  ].join("\n");
571
669
  }
670
+ function measuredTokenCount(agent, coreMessages) {
671
+ const meter = agent.ctx?.get?.("tokenMeter");
672
+ const surface = meter?.measure?.(agent.session)?.surfaceTokens;
673
+ if (typeof surface === "number" && surface > 0) return surface;
674
+ return coreMessages.reduce((sum, message) => sum + defaultCountTokens4(message.text ?? ""), 0);
675
+ }
572
676
  function buildNudge(agent, env, lastNudgeTurn) {
573
677
  const session = agent.session;
574
678
  const state = env.store.stateFor(session);
575
679
  const coreMessages = eventsToCoreMessages(surfaceEventsOf(session));
576
- const tokenCount = coreMessages.reduce((sum, message2) => sum + estimateTokensFast3(message2.text ?? ""), 0);
680
+ const tokenCount = measuredTokenCount(agent, coreMessages);
577
681
  const config = kernelConfigFor(env);
578
682
  const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
579
683
  env.store.set(session, turn.state);
@@ -584,18 +688,18 @@ function buildNudge(agent, env, lastNudgeTurn) {
584
688
  const alreadyShown = !emergency && lastNudgeTurn.get(session.id) === turnNumber;
585
689
  if (alreadyShown) return null;
586
690
  lastNudgeTurn.set(session.id, turnNumber);
587
- const text = buildNudgeText(nudge, emergency, turn.state);
691
+ const text = buildNudgeText(nudge, emergency, session);
588
692
  const message = createUserMessage2({
589
693
  content: [{ type: "text", text }],
590
694
  source: { kind: "plugin", plugin: "acp-nudge" }
591
695
  });
592
696
  return { message, emergency };
593
697
  }
594
- function buildNudgeText(nudge, emergency, state) {
595
- const pct = Math.round(nudge.contextUsage * 100);
698
+ function buildNudgeText(nudge, emergency, session) {
699
+ const pct = Math.round(Math.min(nudge.contextUsage, 1) * 100);
596
700
  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
701
  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");
702
+ return [frame, "", guidance, rangeTable(session)].join("\n");
599
703
  }
600
704
 
601
705
  // src/system-prompt.ts