billion-context-dsh 0.2.0 → 0.2.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/README.en.md +7 -6
- package/README.md +7 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +389 -30
- package/dist/index.js.map +1 -1
- package/dist/nudge.d.ts +11 -3
- package/dist/prompts.d.ts +10 -4
- package/dist/region.d.ts +44 -0
- package/dist/system-prompt.d.ts +3 -3
- package/dist/tools.d.ts +7 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
toolPairingBalancedAfter,
|
|
17
17
|
toolPairingBalancedBefore
|
|
18
18
|
} from "@deepseek-ai/dsh-compaction";
|
|
19
|
-
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
19
|
+
import { createAssistantMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
20
20
|
import { defaultCountTokens } from "acp-kernel";
|
|
21
21
|
|
|
22
22
|
// src/messages.ts
|
|
@@ -347,7 +347,198 @@ function isCheckpointNode(event) {
|
|
|
347
347
|
const source = event.data.source;
|
|
348
348
|
return source?.plugin === "compact";
|
|
349
349
|
}
|
|
350
|
+
function toolCallIdsOfEvent(event) {
|
|
351
|
+
if (event.type !== "assistant/message") return [];
|
|
352
|
+
const content = event.data.message?.content;
|
|
353
|
+
if (!Array.isArray(content)) return [];
|
|
354
|
+
const ids = [];
|
|
355
|
+
for (const block of content) {
|
|
356
|
+
if (block === null || typeof block !== "object") continue;
|
|
357
|
+
const b = block;
|
|
358
|
+
if (b.type === "tool-call" && typeof b.id === "string") ids.push(b.id);
|
|
359
|
+
}
|
|
360
|
+
return ids;
|
|
361
|
+
}
|
|
362
|
+
function toolCallIdOfResultEvent(event) {
|
|
363
|
+
if (event.type !== "tool/result") return null;
|
|
364
|
+
const message = event.data.message;
|
|
365
|
+
const block = Array.isArray(message?.content) ? message.content.find((candidate) => candidate?.type === "tool-result") : void 0;
|
|
366
|
+
const id = block?.toolCallId ?? message?.source?.callId;
|
|
367
|
+
return typeof id === "string" ? id : null;
|
|
368
|
+
}
|
|
369
|
+
function assistantProviderModel(event) {
|
|
370
|
+
if (event.type === "assistant/message") {
|
|
371
|
+
const message = event.data.message;
|
|
372
|
+
return {
|
|
373
|
+
provider: typeof message?.source?.provider === "string" ? message.source.provider : "billion-context-dsh",
|
|
374
|
+
model: typeof message?.source?.model === "string" ? message.source.model : "surface-prune"
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
return { provider: "billion-context-dsh", model: "surface-prune" };
|
|
378
|
+
}
|
|
379
|
+
function hideSurfaceSeqs(session, seqs, provider, model, text) {
|
|
380
|
+
if (seqs.length === 0) return;
|
|
381
|
+
const start = seqs[0];
|
|
382
|
+
const end = seqs[seqs.length - 1];
|
|
383
|
+
let shadowedTokenCount = 0;
|
|
384
|
+
for (const seq of seqs) {
|
|
385
|
+
const event = session.events[seq];
|
|
386
|
+
if (event !== void 0) shadowedTokenCount += defaultCountTokens(extractEventText(event));
|
|
387
|
+
}
|
|
388
|
+
session.append("compaction/prune", {
|
|
389
|
+
shadowedRange: { start, end },
|
|
390
|
+
shadowedSeqs: [...seqs],
|
|
391
|
+
shadowedTokenCount
|
|
392
|
+
});
|
|
393
|
+
if (text !== void 0) {
|
|
394
|
+
session.append("user/message", createUserMessage({
|
|
395
|
+
content: [{ type: "text", text }],
|
|
396
|
+
source: { kind: "plugin", plugin: "billion-context-dsh" }
|
|
397
|
+
}), {
|
|
398
|
+
surfaceOp: { op: "replace", start, end },
|
|
399
|
+
sourceEventSeqs: [...seqs]
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
session.append("assistant/message", {
|
|
404
|
+
turn: findOpenTurn(session.events) ?? 0,
|
|
405
|
+
step: 0,
|
|
406
|
+
message: createAssistantMessage({ content: [], source: { provider, model } })
|
|
407
|
+
}, {
|
|
408
|
+
surfaceOp: { op: "replace", start, end },
|
|
409
|
+
sourceEventSeqs: [...seqs]
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
function hideCompressToolPair(session, callId, resultSeq) {
|
|
413
|
+
let callSeq = null;
|
|
414
|
+
for (const event of session.events) {
|
|
415
|
+
if (event.type !== "assistant/message") continue;
|
|
416
|
+
if (toolCallIdsOfEvent(event).includes(callId)) {
|
|
417
|
+
callSeq = event.seq;
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (callSeq === null) return false;
|
|
422
|
+
const callNodeIds = toolCallIdsOfEvent(session.events[callSeq]);
|
|
423
|
+
if (callNodeIds.length !== 1 || callNodeIds[0] !== callId) return false;
|
|
424
|
+
let resolvedResultSeq = resultSeq ?? null;
|
|
425
|
+
if (resolvedResultSeq === null) {
|
|
426
|
+
for (const event of session.events) {
|
|
427
|
+
if (event.type === "tool/result" && toolCallIdOfResultEvent(event) === callId) {
|
|
428
|
+
resolvedResultSeq = event.seq;
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (resolvedResultSeq === null) return false;
|
|
434
|
+
const nodes = session.surface.nodes;
|
|
435
|
+
const startIdx = nodes.indexOf(callSeq);
|
|
436
|
+
const endIdx = nodes.indexOf(resolvedResultSeq);
|
|
437
|
+
if (startIdx < 0 || endIdx < 0 || endIdx - startIdx !== 1) return false;
|
|
438
|
+
const { provider, model } = assistantProviderModel(session.events[callSeq]);
|
|
439
|
+
const resultEvent = session.events[resolvedResultSeq];
|
|
440
|
+
const resultText = resultEvent === void 0 ? "" : extractEventText(resultEvent);
|
|
441
|
+
hideSurfaceSeqs(session, [callSeq, resolvedResultSeq], provider, model, resultText.trim().length > 0 ? resultText : void 0);
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
function stripOrphanedSurfaceToolMessages(session, inFlightCallIds = /* @__PURE__ */ new Set()) {
|
|
445
|
+
const nodes = session.surface.nodes;
|
|
446
|
+
const callIdsBySeq = /* @__PURE__ */ new Map();
|
|
447
|
+
const open = /* @__PURE__ */ new Map();
|
|
448
|
+
const orphanResultSeqs = [];
|
|
449
|
+
const brokenResults = /* @__PURE__ */ new Map();
|
|
450
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
451
|
+
const seq = nodes[index];
|
|
452
|
+
const event = session.events[seq];
|
|
453
|
+
if (event === void 0) continue;
|
|
454
|
+
if (event.type === "assistant/message") {
|
|
455
|
+
const ids = toolCallIdsOfEvent(event);
|
|
456
|
+
if (ids.length === 0) continue;
|
|
457
|
+
callIdsBySeq.set(seq, ids);
|
|
458
|
+
for (const id of ids) {
|
|
459
|
+
if (!open.has(id)) open.set(id, { seq, index });
|
|
460
|
+
}
|
|
461
|
+
} else if (event.type === "tool/result") {
|
|
462
|
+
const id = toolCallIdOfResultEvent(event);
|
|
463
|
+
if (id === null) continue;
|
|
464
|
+
const call = open.get(id);
|
|
465
|
+
if (call === void 0) {
|
|
466
|
+
orphanResultSeqs.push(seq);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const callNodeIds = callIdsBySeq.get(call.seq);
|
|
470
|
+
let adjacent = false;
|
|
471
|
+
if (callNodeIds !== void 0) {
|
|
472
|
+
adjacent = true;
|
|
473
|
+
for (let mid = call.index + 1; mid < index; mid += 1) {
|
|
474
|
+
const midEvent = session.events[nodes[mid]];
|
|
475
|
+
if (midEvent === void 0 || midEvent.type !== "tool/result") {
|
|
476
|
+
adjacent = false;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
const midId = toolCallIdOfResultEvent(midEvent);
|
|
480
|
+
if (midId === null || !callNodeIds.includes(midId)) {
|
|
481
|
+
adjacent = false;
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
open.delete(id);
|
|
487
|
+
if (!adjacent) brokenResults.set(seq, call.seq);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const brokenIdsByCallSeq = /* @__PURE__ */ new Map();
|
|
491
|
+
for (const [resultSeq, callSeq] of brokenResults) {
|
|
492
|
+
const id = toolCallIdOfResultEvent(session.events[resultSeq]);
|
|
493
|
+
if (id !== null) {
|
|
494
|
+
const list = brokenIdsByCallSeq.get(callSeq) ?? [];
|
|
495
|
+
list.push(id);
|
|
496
|
+
brokenIdsByCallSeq.set(callSeq, list);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const hiddenSet = new Set(orphanResultSeqs);
|
|
500
|
+
for (const resultSeq of brokenResults.keys()) hiddenSet.add(resultSeq);
|
|
501
|
+
for (const [callSeq, ids] of callIdsBySeq) {
|
|
502
|
+
const brokenIds = brokenIdsByCallSeq.get(callSeq);
|
|
503
|
+
const allUnpaired = !ids.some((candidate) => inFlightCallIds.has(candidate)) && ids.every((candidate) => open.has(candidate) || brokenIds?.includes(candidate) === true);
|
|
504
|
+
if (allUnpaired) hiddenSet.add(callSeq);
|
|
505
|
+
}
|
|
506
|
+
const hidden = [...hiddenSet].sort((a, b) => a - b);
|
|
507
|
+
let count = 0;
|
|
508
|
+
for (const seq of hidden) {
|
|
509
|
+
const event = session.events[seq];
|
|
510
|
+
if (event === void 0) continue;
|
|
511
|
+
const { provider, model } = assistantProviderModel(event);
|
|
512
|
+
hideSurfaceSeqs(session, [seq], provider, model);
|
|
513
|
+
count += 1;
|
|
514
|
+
}
|
|
515
|
+
return count;
|
|
516
|
+
}
|
|
517
|
+
function openToolCallIds(session) {
|
|
518
|
+
const open = /* @__PURE__ */ new Set();
|
|
519
|
+
for (const seq of session.surface.nodes) {
|
|
520
|
+
const event = session.events[seq];
|
|
521
|
+
if (event === void 0) continue;
|
|
522
|
+
if (event.type === "assistant/message") {
|
|
523
|
+
for (const id of toolCallIdsOfEvent(event)) open.add(id);
|
|
524
|
+
} else if (event.type === "tool/result") {
|
|
525
|
+
const id = toolCallIdOfResultEvent(event);
|
|
526
|
+
if (id !== null) open.delete(id);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return open;
|
|
530
|
+
}
|
|
531
|
+
function deferCompressPairHide(session, callId, resultSeq, onError) {
|
|
532
|
+
queueMicrotask(() => {
|
|
533
|
+
try {
|
|
534
|
+
hideCompressToolPair(session, callId, resultSeq);
|
|
535
|
+
} catch (error) {
|
|
536
|
+
onError?.(error);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
}
|
|
350
540
|
function buildCompressibleSeqRanges(session, opts = {}) {
|
|
541
|
+
stripOrphanedSurfaceToolMessages(session);
|
|
351
542
|
const nodes = session.surface.nodes;
|
|
352
543
|
const preserve = opts.preserveRecent ?? 5;
|
|
353
544
|
const protectedSeqs = /* @__PURE__ */ new Set();
|
|
@@ -582,17 +773,24 @@ function kernelConfigFor(input) {
|
|
|
582
773
|
|
|
583
774
|
// src/nudge.ts
|
|
584
775
|
import {
|
|
585
|
-
|
|
776
|
+
COMPRESS_PHILOSOPHY as COMPRESS_PHILOSOPHY2,
|
|
777
|
+
TIER2_DISTILL_RULES as TIER2_DISTILL_RULES2,
|
|
778
|
+
TIER3_CONDENSE_RULES as TIER3_CONDENSE_RULES2,
|
|
779
|
+
defaultCountTokens as defaultCountTokens2,
|
|
780
|
+
renderNudgeText
|
|
586
781
|
} from "acp-kernel";
|
|
587
782
|
import { createUserMessage as createUserMessage2 } from "@deepseek-ai/dsh-llm";
|
|
588
783
|
|
|
589
784
|
// src/prompts.ts
|
|
590
|
-
import { COMPRESS_PHILOSOPHY } from "acp-kernel";
|
|
785
|
+
import { COMPRESS_PHILOSOPHY, HOW_TO_COMPRESS_RULES, TIER2_DISTILL_RULES, TIER3_CONDENSE_RULES } from "acp-kernel";
|
|
591
786
|
var NUDGE_ALLOWED = {
|
|
592
|
-
normal: /* @__PURE__ */ new Set(["pct"]),
|
|
593
|
-
emergency: /* @__PURE__ */ new Set(["pct"]),
|
|
787
|
+
normal: /* @__PURE__ */ new Set(["pct", "philosophy"]),
|
|
788
|
+
emergency: /* @__PURE__ */ new Set(["pct", "philosophy"]),
|
|
594
789
|
guidance: /* @__PURE__ */ new Set(),
|
|
595
|
-
tier: /* @__PURE__ */ new Set(["tier", "count", "prevTier", "tokens", "seqs"])
|
|
790
|
+
tier: /* @__PURE__ */ new Set(["tier", "count", "prevTier", "tokens", "seqs"]),
|
|
791
|
+
breakdown: /* @__PURE__ */ new Set(["system", "tool", "summaries", "code", "text"]),
|
|
792
|
+
growth: /* @__PURE__ */ new Set(["growth"]),
|
|
793
|
+
tip: /* @__PURE__ */ new Set()
|
|
596
794
|
};
|
|
597
795
|
var RANGE_TABLE_ALLOWED = {
|
|
598
796
|
header: /* @__PURE__ */ new Set(["surface"]),
|
|
@@ -606,7 +804,7 @@ var TOOLS_ALLOWED = {
|
|
|
606
804
|
searchContext: /* @__PURE__ */ new Set(),
|
|
607
805
|
acpStatus: /* @__PURE__ */ new Set()
|
|
608
806
|
};
|
|
609
|
-
var SYSTEM_ALLOWED = /* @__PURE__ */ new Set(["philosophy"]);
|
|
807
|
+
var SYSTEM_ALLOWED = /* @__PURE__ */ new Set(["philosophy", "howToCompressRules", "tier2DistillRules", "tier3CondenseRules"]);
|
|
610
808
|
function validateTemplate(template, allowed, path) {
|
|
611
809
|
const re = /\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
612
810
|
let match;
|
|
@@ -650,14 +848,24 @@ function resolvePrompts(input) {
|
|
|
650
848
|
};
|
|
651
849
|
}
|
|
652
850
|
function renderSystemPrompt(prompts) {
|
|
653
|
-
return renderTemplate(prompts.systemPromptTemplate, {
|
|
851
|
+
return renderTemplate(prompts.systemPromptTemplate, {
|
|
852
|
+
philosophy: COMPRESS_PHILOSOPHY,
|
|
853
|
+
howToCompressRules: HOW_TO_COMPRESS_RULES,
|
|
854
|
+
tier2DistillRules: TIER2_DISTILL_RULES,
|
|
855
|
+
tier3CondenseRules: TIER3_CONDENSE_RULES
|
|
856
|
+
});
|
|
654
857
|
}
|
|
655
858
|
var DEFAULT_PROMPTS = {
|
|
656
859
|
nudge: {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
860
|
+
// 与 kernel nudge-text.ts EFFICIENCY_NOTE 逐字对齐——不含 "Context usage is at X%"
|
|
861
|
+
// 陈述(usage 只通过 breakdown 传达);{pct} 仍可用作自定义占位符。
|
|
862
|
+
normal: "This is an efficiency nudge to compress early and keep context lean \u2014 not an overflow warning. A separate, stronger alert will appear if the context is actually full.\n\n{philosophy}",
|
|
863
|
+
emergency: "\u26A0\uFE0F Context limit reached \u2014 compress now. Prioritize consumed tool outputs.\n\n{philosophy}",
|
|
864
|
+
guidance: HOW_TO_COMPRESS_RULES,
|
|
865
|
+
tier: "Tier {tier}: {count} tier-{prevTier} block(s) distillable ({tokens} tokens) \u2014 compress their summary node(s) [seqs {seqs}] to reclaim the original messages.",
|
|
866
|
+
breakdown: "Context breakdown: {system}K system | {tool}K tool | {summaries}K summaries | {code}K code | {text}K text",
|
|
867
|
+
growth: "+{growth}K since last nudge",
|
|
868
|
+
tip: "\u{1F4A1} Compress all ranges in one call (pass multiple content entries: `content: [{...}, {...}]`)."
|
|
661
869
|
},
|
|
662
870
|
rangeTable: {
|
|
663
871
|
header: "Surface: {surface}",
|
|
@@ -673,10 +881,26 @@ var DEFAULT_PROMPTS = {
|
|
|
673
881
|
},
|
|
674
882
|
systemPromptTemplate: `Active Context Pruning \u2014 model-driven context management
|
|
675
883
|
|
|
676
|
-
YOU decide whether and when to compress context.
|
|
884
|
+
YOU decide whether and when to compress context. The nudge is an efficiency notification: when you see one, consider which ranges you have genuinely consumed and could summarise to keep working context lean.
|
|
677
885
|
|
|
678
886
|
{philosophy}
|
|
679
887
|
|
|
888
|
+
WHEN TO COMPRESS:
|
|
889
|
+
- A sub-agent or delegated task has returned a large result that you have already extracted the key facts from.
|
|
890
|
+
- Verbose command output (build/test logs, git diff, directory listings) where you have already used the information you need.
|
|
891
|
+
- Exploration that led nowhere.
|
|
892
|
+
- Repeated reads of the same file or repeated status checks once the decision is recorded.
|
|
893
|
+
- Resolved discussion threads where a decision has been captured in summary or in code.
|
|
894
|
+
- Intermediate steps of a completed multi-step task, once the final result is recorded.
|
|
895
|
+
- A task phase has ended \u2014 bug hunt complete, root cause found, exploration done, research sprint wrapped.
|
|
896
|
+
|
|
897
|
+
WHEN NOT TO COMPRESS:
|
|
898
|
+
- Content the current step is actively reading or reasoning about.
|
|
899
|
+
- Important user messages \u2014 preserve their exact intent, constraints, and acceptance criteria.
|
|
900
|
+
- Protected tool outputs \u2014 hard-excluded from compression ranges, survive intact in visible context.
|
|
901
|
+
|
|
902
|
+
{howToCompressRules}
|
|
903
|
+
|
|
680
904
|
Compression tools (refs are SURFACE SEQS, not ids):
|
|
681
905
|
- 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.
|
|
682
906
|
- decompress: recover a compressed block's original content, read-only. decompress({ blockId }).
|
|
@@ -685,6 +909,10 @@ Compression tools (refs are SURFACE SEQS, not ids):
|
|
|
685
909
|
|
|
686
910
|
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.
|
|
687
911
|
|
|
912
|
+
{tier2DistillRules}
|
|
913
|
+
|
|
914
|
+
{tier3CondenseRules}
|
|
915
|
+
|
|
688
916
|
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.`
|
|
689
917
|
};
|
|
690
918
|
var DEFAULT_RESOLVED = DEFAULT_PROMPTS;
|
|
@@ -746,12 +974,84 @@ function buildNudge(agent, env, lastNudgeTurn) {
|
|
|
746
974
|
return { message, emergency };
|
|
747
975
|
}
|
|
748
976
|
function buildNudgeText(nudge, emergency, session, prompts = DEFAULT_RESOLVED) {
|
|
977
|
+
if (prompts.nudge !== DEFAULT_RESOLVED.nudge) {
|
|
978
|
+
return renderNudgeFromTemplates(nudge, emergency, session, prompts);
|
|
979
|
+
}
|
|
980
|
+
const rendered = renderNudgeText(nudge);
|
|
981
|
+
return adaptKernelNudgeToSeq(rendered.text, nudge, session, prompts);
|
|
982
|
+
}
|
|
983
|
+
function adaptKernelNudgeToSeq(text, nudge, session, prompts) {
|
|
984
|
+
let out = text;
|
|
985
|
+
if ((nudge.tier === 2 || nudge.tier === 3) && (nudge.tierTargetBlocks?.length ?? 0) > 0) {
|
|
986
|
+
out = replaceTierTrigger(out, nudge, session, prompts);
|
|
987
|
+
} else if (out.includes('"startId"')) {
|
|
988
|
+
out = replaceEmergencyExample(out);
|
|
989
|
+
}
|
|
990
|
+
const seqTable = rangeTable(session, prompts);
|
|
991
|
+
if (seqTable !== "") out = replaceRangesStr(out, seqTable);
|
|
992
|
+
return out;
|
|
993
|
+
}
|
|
994
|
+
function replaceRangesStr(text, seqTable) {
|
|
995
|
+
const match = text.match(/\n\n(?:Compressible ranges \(|\[No specific ranges detected)/);
|
|
996
|
+
if (!match) return text;
|
|
997
|
+
const start = match.index;
|
|
998
|
+
const rest = text.slice(start + 2);
|
|
999
|
+
const next = rest.match(/\n\n/);
|
|
1000
|
+
const end = next !== null ? start + 2 + next.index : text.length;
|
|
1001
|
+
const before = text.slice(0, start);
|
|
1002
|
+
const after = text.slice(end);
|
|
1003
|
+
return before + "\n" + seqTable + after;
|
|
1004
|
+
}
|
|
1005
|
+
function replaceTierTrigger(text, nudge, session, prompts) {
|
|
1006
|
+
const start = text.search(/\n\n(?:\[TIER \d|\[EMERGENCY — TIER \d)/);
|
|
1007
|
+
if (start === -1) return text;
|
|
1008
|
+
const rest = text.slice(start + 2);
|
|
1009
|
+
const next = rest.match(/\n\nHOW TO COMPRESS/);
|
|
1010
|
+
const end = next !== null ? start + 2 + next.index : text.length;
|
|
1011
|
+
const targets = nudge.tierTargetBlocks;
|
|
1012
|
+
const summarySeqs = targets.map((block) => summarySeqOfKernelBlock(session, block.blockId)).filter((seq) => seq !== null);
|
|
1013
|
+
const pending = nudge.tier === 2 ? nudge.breakdown?.pendingT2 : nudge.breakdown?.pendingT3;
|
|
1014
|
+
const tokens = typeof pending === "number" ? pending : 0;
|
|
1015
|
+
const tierValue = nudge.tier === null ? 2 : nudge.tier;
|
|
1016
|
+
const tierLine = renderTemplate(prompts.nudge.tier, {
|
|
1017
|
+
tier: tierValue,
|
|
1018
|
+
count: targets.length,
|
|
1019
|
+
prevTier: tierValue - 1,
|
|
1020
|
+
tokens,
|
|
1021
|
+
seqs: summarySeqs.join(", ")
|
|
1022
|
+
});
|
|
1023
|
+
return text.slice(0, start) + "\n\n" + tierLine + text.slice(end);
|
|
1024
|
+
}
|
|
1025
|
+
function replaceEmergencyExample(text) {
|
|
1026
|
+
const start = text.search(/\n\n\{ "topic":/);
|
|
1027
|
+
if (start === -1) return text;
|
|
1028
|
+
const rest = text.slice(start + 2);
|
|
1029
|
+
const next = rest.match(/\n\nCompressible ranges |\n\n\[No specific/);
|
|
1030
|
+
const end = next !== null ? start + 2 + next.index : text.length;
|
|
1031
|
+
return text.slice(0, start) + "\n\ncompress({ content: [{ startSeq, endSeq, summary }] }) \u2014 use the seqs from the range table above." + text.slice(end);
|
|
1032
|
+
}
|
|
1033
|
+
function renderNudgeFromTemplates(nudge, emergency, session, prompts) {
|
|
749
1034
|
const pct = Math.round(Math.min(nudge.contextUsage, 1) * 100);
|
|
750
1035
|
const frame = renderTemplate(
|
|
751
1036
|
emergency ? prompts.nudge.emergency : prompts.nudge.normal,
|
|
752
|
-
{ pct }
|
|
1037
|
+
{ pct, philosophy: COMPRESS_PHILOSOPHY2 }
|
|
753
1038
|
);
|
|
754
1039
|
const parts = [frame];
|
|
1040
|
+
if (nudge.contextBreakdown) {
|
|
1041
|
+
const bd = nudge.contextBreakdown;
|
|
1042
|
+
const breakdown = renderTemplate(prompts.nudge.breakdown, {
|
|
1043
|
+
system: Math.round(bd.system / 1e3),
|
|
1044
|
+
tool: Math.round(bd.tool / 1e3),
|
|
1045
|
+
summaries: Math.round(bd.summaries / 1e3),
|
|
1046
|
+
code: Math.round(bd.code / 1e3),
|
|
1047
|
+
text: Math.round(bd.text / 1e3)
|
|
1048
|
+
});
|
|
1049
|
+
if (breakdown !== "") parts.push("", breakdown);
|
|
1050
|
+
if (bd.growth > 0) {
|
|
1051
|
+
const growth = renderTemplate(prompts.nudge.growth, { growth: Math.round(bd.growth / 1e3) });
|
|
1052
|
+
if (growth !== "") parts.push(growth);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
755
1055
|
if (prompts.nudge.guidance !== "") parts.push("", prompts.nudge.guidance);
|
|
756
1056
|
if ((nudge.tier === 2 || nudge.tier === 3) && (nudge.tierTargetBlocks?.length ?? 0) > 0) {
|
|
757
1057
|
const targets = nudge.tierTargetBlocks;
|
|
@@ -766,8 +1066,12 @@ function buildNudgeText(nudge, emergency, session, prompts = DEFAULT_RESOLVED) {
|
|
|
766
1066
|
seqs: summarySeqs.join(", ")
|
|
767
1067
|
});
|
|
768
1068
|
if (tierLine !== "") parts.push(tierLine);
|
|
1069
|
+
const tierRules = nudge.tier === 2 ? TIER2_DISTILL_RULES2 : TIER3_CONDENSE_RULES2;
|
|
1070
|
+
parts.push("", tierRules);
|
|
1071
|
+
} else {
|
|
1072
|
+
parts.push(rangeTable(session, prompts));
|
|
769
1073
|
}
|
|
770
|
-
parts.push(
|
|
1074
|
+
if (prompts.nudge.tip !== "") parts.push("", prompts.nudge.tip);
|
|
771
1075
|
return parts.join("\n");
|
|
772
1076
|
}
|
|
773
1077
|
|
|
@@ -811,11 +1115,22 @@ function requireAgent(exec) {
|
|
|
811
1115
|
return exec.agent;
|
|
812
1116
|
}
|
|
813
1117
|
var compressParameters = {
|
|
1118
|
+
// Tolerated wrapped-arguments form: some models emit
|
|
1119
|
+
// `{ "arguments": "{\"content\": [...]}" }` (double-nested) or
|
|
1120
|
+
// `{ "arguments": { "content": [...] } }` instead of the unwrapped
|
|
1121
|
+
// `{ "content": [...] }`. The old DSH validator surfaced this as
|
|
1122
|
+
// `invalid arguments: "arguments" must be an object` and the model retried
|
|
1123
|
+
// forever. `arguments` is accepted as an optional JSON node so the wrapped
|
|
1124
|
+
// shape passes schema validation; `handleCompress` unwraps it and falls back
|
|
1125
|
+
// to a clear runtime error when neither form carries content. `content` is
|
|
1126
|
+
// intentionally NOT `required: true` — a required property would reject the
|
|
1127
|
+
// wrapped shape before `handleCompress` can see it. The tool description
|
|
1128
|
+
// still tells the model content is mandatory.
|
|
1129
|
+
arguments: { type: "json", description: "Tolerated wrapped-arguments form (model-generated); unwrapped in handleCompress. Prefer passing content directly." },
|
|
814
1130
|
topic: { type: "string", description: "Fallback topic for entries without their own." },
|
|
815
1131
|
content: {
|
|
816
1132
|
type: "array",
|
|
817
|
-
|
|
818
|
-
description: "One or more ranges to compress, each with startSeq/endSeq boundaries (surface seqs) and a dense summary.",
|
|
1133
|
+
description: "One or more ranges to compress, each with startSeq/endSeq boundaries (surface seqs) and a dense summary. Required \u2014 pass it directly, not wrapped in an arguments key.",
|
|
819
1134
|
items: {
|
|
820
1135
|
type: "object",
|
|
821
1136
|
properties: {
|
|
@@ -846,9 +1161,26 @@ function parseSeq(value) {
|
|
|
846
1161
|
}
|
|
847
1162
|
return seq;
|
|
848
1163
|
}
|
|
1164
|
+
function unwrapCompressArgs(args) {
|
|
1165
|
+
if (args.content !== void 0) return args;
|
|
1166
|
+
if (args.arguments === void 0) return null;
|
|
1167
|
+
let inner = args.arguments;
|
|
1168
|
+
if (typeof inner === "string") {
|
|
1169
|
+
try {
|
|
1170
|
+
inner = JSON.parse(inner);
|
|
1171
|
+
} catch {
|
|
1172
|
+
return null;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (typeof inner !== "object" || inner === null || Array.isArray(inner)) return null;
|
|
1176
|
+
const content = inner.content;
|
|
1177
|
+
if (content === void 0) return null;
|
|
1178
|
+
return { ...args, content };
|
|
1179
|
+
}
|
|
849
1180
|
async function handleCompress(env, args, exec) {
|
|
850
1181
|
const agent = requireAgent(exec);
|
|
851
1182
|
const session = agent.session;
|
|
1183
|
+
stripOrphanedSurfaceToolMessages(session, openToolCallIds(session));
|
|
852
1184
|
const state = env.store.stateFor(session);
|
|
853
1185
|
const coreMessages = allLogMessages(session);
|
|
854
1186
|
const surfaceMessages = eventsToCoreMessages(surfaceEventsOf(session));
|
|
@@ -857,6 +1189,13 @@ async function handleCompress(env, args, exec) {
|
|
|
857
1189
|
const turn = env.kernel.processTurn({ messages: coreMessages, state, config, tokenCount });
|
|
858
1190
|
env.store.set(session, turn.state);
|
|
859
1191
|
const byRaw = turn.state.messageRefs.byRaw;
|
|
1192
|
+
const unwrapped = unwrapCompressArgs(args);
|
|
1193
|
+
if (unwrapped === null) {
|
|
1194
|
+
return {
|
|
1195
|
+
text: "compress: missing content \u2014 pass the content array directly: compress({ content: [{ startSeq, endSeq, summary }] })"
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
args = unwrapped;
|
|
860
1199
|
const ranges = [];
|
|
861
1200
|
const alreadyCompressedNotes = [];
|
|
862
1201
|
for (const range of args.content) {
|
|
@@ -912,10 +1251,13 @@ async function handleCompress(env, args, exec) {
|
|
|
912
1251
|
// non-block-covered messages as the visible feed, so default behavior is
|
|
913
1252
|
// preserved. Any 'Excluded N protected message(s)' warning is surfaced.
|
|
914
1253
|
});
|
|
915
|
-
if (applied.result.errors.length > 0) {
|
|
1254
|
+
if (applied.result.errors.length > 0 && applied.result.blocksCreated === 0) {
|
|
916
1255
|
return { text: `compress failed: ${applied.result.errors.join("; ")}` };
|
|
917
1256
|
}
|
|
918
1257
|
env.store.set(session, applied.state);
|
|
1258
|
+
if (applied.result.blocksCreated > 0) {
|
|
1259
|
+
env.compressCallIdsToHide?.add(exec.callId);
|
|
1260
|
+
}
|
|
919
1261
|
const previousIds = new Set(turn.state.blocks.map((block) => block.blockId));
|
|
920
1262
|
const newBlocks = applied.state.blocks.filter((block) => !previousIds.has(block.blockId));
|
|
921
1263
|
const blockByRangeKey = new Map(newBlocks.map((block) => [`${block.startRef}::${block.endRef}`, block]));
|
|
@@ -978,8 +1320,9 @@ async function handleCompress(env, args, exec) {
|
|
|
978
1320
|
}
|
|
979
1321
|
const summaryLine = `Compressed ${applied.result.blocksCreated} block(s), ~${applied.result.tokensCompressed} tokens reclaimed.`;
|
|
980
1322
|
const totalSkipped = skippedRanges + alreadyCompressedNotes.length;
|
|
981
|
-
const
|
|
982
|
-
const
|
|
1323
|
+
const failedLines = applied.result.errors.map((error) => ` ${error}`);
|
|
1324
|
+
const warningLines = [...freeWarnings.map((warning) => ` ${warning}`), ...failedLines, ...alreadyCompressedNotes, ...lines];
|
|
1325
|
+
const footer = totalSkipped > 0 ? ` (${totalSkipped} range(s) skipped or failed \u2014 see above)` : "";
|
|
983
1326
|
return { text: `${summaryLine}
|
|
984
1327
|
${[...warningLines, footer].filter((line) => line !== "").join("\n")}` };
|
|
985
1328
|
}
|
|
@@ -1213,6 +1556,8 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
1213
1556
|
/** Resolved prompt templates (validated at construction — fail-fast on template typos). */
|
|
1214
1557
|
prompts;
|
|
1215
1558
|
lastNudgeTurn = /* @__PURE__ */ new Map();
|
|
1559
|
+
/** Successful compress call ids awaiting their tool/result so the pair can be hidden. */
|
|
1560
|
+
compressCallIdsToHide = /* @__PURE__ */ new Set();
|
|
1216
1561
|
/** Per provider/model route the resolved window (probe failures cached too). */
|
|
1217
1562
|
windowCache = /* @__PURE__ */ new Map();
|
|
1218
1563
|
constructor(ctx, config = {}) {
|
|
@@ -1232,7 +1577,8 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
1232
1577
|
nudgeEmergencyThresholdPct: this.config.nudgeEmergencyThresholdPct,
|
|
1233
1578
|
coreOverrides: this.config.coreOverrides,
|
|
1234
1579
|
windowFor: (agent) => this.windowFor(agent),
|
|
1235
|
-
prompts: this.prompts
|
|
1580
|
+
prompts: this.prompts,
|
|
1581
|
+
compressCallIdsToHide: this.compressCallIdsToHide
|
|
1236
1582
|
};
|
|
1237
1583
|
const tools = ctx.get("tools");
|
|
1238
1584
|
if (tools !== void 0) {
|
|
@@ -1266,16 +1612,27 @@ var AcpCompactionEngine = class extends CompactionEngine {
|
|
|
1266
1612
|
if (name === "commands") registerCommand();
|
|
1267
1613
|
});
|
|
1268
1614
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1615
|
+
ctx.on("session/event", (session, event) => {
|
|
1616
|
+
if (event.type !== "tool/result") return;
|
|
1617
|
+
const message = event.data.message;
|
|
1618
|
+
const block = message.content[0];
|
|
1619
|
+
const callId = block?.toolCallId ?? message.source.callId;
|
|
1620
|
+
if (typeof callId !== "string" || !this.compressCallIdsToHide.has(callId)) return;
|
|
1621
|
+
this.compressCallIdsToHide.delete(callId);
|
|
1622
|
+
deferCompressPairHide(session, callId, event.seq, (error) => {
|
|
1623
|
+
ctx.logger.warn(`billion-context-dsh: hide compress call/result pair failed: ${String(error)}`);
|
|
1277
1624
|
});
|
|
1278
|
-
}
|
|
1625
|
+
});
|
|
1626
|
+
ctx.on("agent/pre-step", async (payload, next) => {
|
|
1627
|
+
stripOrphanedSurfaceToolMessages(payload.agent.session);
|
|
1628
|
+
if (!this.config.autoNudge) return next();
|
|
1629
|
+
const decision = await next();
|
|
1630
|
+
if (decision.kind === "reject") return decision;
|
|
1631
|
+
const window = await this.windowFor(payload.agent);
|
|
1632
|
+
const outcome = buildNudge(payload.agent, { ...env, modelContextLimit: window.limit }, this.lastNudgeTurn);
|
|
1633
|
+
if (outcome === null) return decision;
|
|
1634
|
+
return { kind: "enter", messages: [...decision.messages, outcome.message] };
|
|
1635
|
+
});
|
|
1279
1636
|
const systemPrompt = ctx.get("systemPrompt");
|
|
1280
1637
|
if (systemPrompt !== void 0) {
|
|
1281
1638
|
systemPrompt.section({
|
|
@@ -1372,6 +1729,7 @@ export {
|
|
|
1372
1729
|
expandShadowedSeqs,
|
|
1373
1730
|
extractEventText,
|
|
1374
1731
|
findOpenTurn,
|
|
1732
|
+
hideCompressToolPair,
|
|
1375
1733
|
kernelConfigFor,
|
|
1376
1734
|
makeTools,
|
|
1377
1735
|
projectEvent,
|
|
@@ -1384,6 +1742,7 @@ export {
|
|
|
1384
1742
|
resolveTokenCount,
|
|
1385
1743
|
runCompactionTransaction,
|
|
1386
1744
|
shadowedSeqsOf,
|
|
1745
|
+
stripOrphanedSurfaceToolMessages,
|
|
1387
1746
|
summarySeqOfKernelBlock,
|
|
1388
1747
|
surfaceEventsOf,
|
|
1389
1748
|
windowSourceLabel
|