@logictan/dsh-verbatim-compaction 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +65 -42
- package/lib/types/index.d.ts +1 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -153,9 +153,9 @@ export function isErrorResult(event) {
|
|
|
153
153
|
/** Directive appended as the final user message of the extraction call. */
|
|
154
154
|
const EXTRACT_INSTRUCTION = [
|
|
155
155
|
'You are summarizing a batch of turns from a coding session for a compaction checkpoint.',
|
|
156
|
-
'Each
|
|
156
|
+
'Each item is introduced by a line `=== SLOT <key> (turn <n>) ===` and contains the user request, the assistant prose (may be empty), and a trace of the tool calls it made (descriptions only) plus any results the log marked as errors.',
|
|
157
157
|
'',
|
|
158
|
-
'For EVERY
|
|
158
|
+
'For EVERY item, emit exactly one block, introduced by `=== SLOT <key> ===` using that same key, containing these three Markdown headings in order. Under each, at most two terse lines. If a section has no real information, write "(none)" — never invent.',
|
|
159
159
|
'',
|
|
160
160
|
'## Did',
|
|
161
161
|
'- what was actually done, concretely',
|
|
@@ -362,13 +362,26 @@ export function buildSections(events) {
|
|
|
362
362
|
* @returns the same sections with `extracted` set where available.
|
|
363
363
|
*/
|
|
364
364
|
function withExtractions(sections, extracted) {
|
|
365
|
-
return sections.map((section) => ({
|
|
366
|
-
turns: section.turns.map((turn) => {
|
|
367
|
-
const block = extracted.get(
|
|
365
|
+
return sections.map((section, sectionIndex) => ({
|
|
366
|
+
turns: section.turns.map((turn, turnIndex) => {
|
|
367
|
+
const block = extracted.get(turnKey(sectionIndex, turnIndex));
|
|
368
368
|
return block === undefined ? turn : { ...turn, extracted: block };
|
|
369
369
|
}),
|
|
370
370
|
}));
|
|
371
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* Position-based key for one turn: `<section index>:<turn index>`.
|
|
374
|
+
*
|
|
375
|
+
* Not the turn number. `turn/start` is emitted per step in some sessions and only
|
|
376
|
+
* once in others, so the number in the log is neither unique nor one-per-request.
|
|
377
|
+
* A real session carried 2 user requests under a single `turn/start` (turn 1), so
|
|
378
|
+
* both sections were keyed `1`; the second extraction overwrote the first, and
|
|
379
|
+
* the digest printed the same block twice — once under a request about source
|
|
380
|
+
* refresh, once under 「模拟器使用硬件加速!」.
|
|
381
|
+
*/
|
|
382
|
+
function turnKey(sectionIndex, turnIndex) {
|
|
383
|
+
return `${sectionIndex}:${turnIndex}`;
|
|
384
|
+
}
|
|
372
385
|
/**
|
|
373
386
|
* Render one section to digest text, truncating assistant prose to `capChars`.
|
|
374
387
|
*
|
|
@@ -653,17 +666,20 @@ async function extractBatch(ctx, agent, batch, signal) {
|
|
|
653
666
|
const model = route?.model ?? agent.options.model;
|
|
654
667
|
if (provider === undefined || model === undefined)
|
|
655
668
|
return out;
|
|
669
|
+
// The label the model echoes back is the slot key, not the turn number: two
|
|
670
|
+
// requests can share a number, and a duplicate label would collapse two blocks
|
|
671
|
+
// into one entry.
|
|
656
672
|
const parts = [];
|
|
657
|
-
for (const
|
|
658
|
-
const prose = events
|
|
673
|
+
for (const slot of batch) {
|
|
674
|
+
const prose = slot.events
|
|
659
675
|
.filter((event) => event.type === 'assistant/message')
|
|
660
676
|
.map((event) => textOf(event))
|
|
661
677
|
.filter((text) => text.length > 0)
|
|
662
678
|
.join('\n');
|
|
663
|
-
const material = buildExtractionMaterial(events, prose);
|
|
679
|
+
const material = buildExtractionMaterial(slot.events, prose);
|
|
664
680
|
if (material.length === 0)
|
|
665
681
|
continue;
|
|
666
|
-
parts.push(`===
|
|
682
|
+
parts.push(`=== SLOT ${slot.key} (turn ${slot.turn ?? '?'}) ===\n${material}`);
|
|
667
683
|
}
|
|
668
684
|
if (parts.length === 0)
|
|
669
685
|
return out;
|
|
@@ -675,17 +691,12 @@ async function extractBatch(ctx, agent, batch, signal) {
|
|
|
675
691
|
messages: [
|
|
676
692
|
{ role: 'user', content: [{ type: 'text', text: `${parts.join('\n\n')}\n\n${EXTRACT_INSTRUCTION}` }] },
|
|
677
693
|
],
|
|
678
|
-
// One block per turn, so the cap scales with the batch rather than being
|
|
679
|
-
// a per-turn 400 that a dozen turns would multiply past the digest ceiling.
|
|
680
694
|
maxTokens: EXTRACT_TOKENS_PER_TURN * parts.length,
|
|
681
695
|
sessionId: agent.session.id,
|
|
682
696
|
purpose: 'compaction',
|
|
683
697
|
...(signal === undefined ? {} : { signal }),
|
|
684
698
|
});
|
|
685
699
|
for await (const chunk of stream) {
|
|
686
|
-
// Chunk kinds come from `@deepseek-ai/dsh-llm`'s BlockAssembler: text
|
|
687
|
-
// arrives as `text-delta`, and `reasoning-delta` is deliberately ignored —
|
|
688
|
-
// reasoning is 98.8% of a long session's output and is not the record.
|
|
689
700
|
const delta = chunk;
|
|
690
701
|
if (delta.type === 'text-delta' && typeof delta.text === 'string')
|
|
691
702
|
chunks.push(delta.text);
|
|
@@ -694,17 +705,15 @@ async function extractBatch(ctx, agent, batch, signal) {
|
|
|
694
705
|
catch {
|
|
695
706
|
return out;
|
|
696
707
|
}
|
|
697
|
-
// Split the answer back per turn. A turn the model skipped simply keeps its
|
|
698
|
-
// prose, so a partial answer degrades per turn instead of losing the batch.
|
|
699
708
|
const answer = chunks.join('').trim();
|
|
700
709
|
if (answer.length === 0)
|
|
701
710
|
return out;
|
|
702
|
-
const
|
|
703
|
-
for (let index = 1; index <
|
|
704
|
-
const
|
|
705
|
-
const body = (
|
|
706
|
-
if (
|
|
707
|
-
out.set(
|
|
711
|
+
const blocks = answer.split(/^=== SLOT ([^\s(]+)[^=]*===$/m);
|
|
712
|
+
for (let index = 1; index < blocks.length; index += 2) {
|
|
713
|
+
const key = blocks[index];
|
|
714
|
+
const body = (blocks[index + 1] ?? '').trim();
|
|
715
|
+
if (key !== undefined && body.length > 0)
|
|
716
|
+
out.set(key, body);
|
|
708
717
|
}
|
|
709
718
|
return out;
|
|
710
719
|
}
|
|
@@ -779,33 +788,47 @@ export default class VerbatimCompactionEngine extends BasicCompactionEngine {
|
|
|
779
788
|
* @returns turn number to extracted block, omitting turns that yielded none.
|
|
780
789
|
*/
|
|
781
790
|
async extractAll(source, agent, signal) {
|
|
782
|
-
|
|
791
|
+
// Walk the same sections the renderer will use, so a key addresses the exact
|
|
792
|
+
// slot the block will be written into. Grouping by turn number instead lost
|
|
793
|
+
// blocks whenever the log reused a number.
|
|
794
|
+
const sections = buildSections(source);
|
|
795
|
+
const slots = [];
|
|
783
796
|
let lastTurn;
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
797
|
+
let cursor = 0;
|
|
798
|
+
for (const [sectionIndex, section] of sections.entries()) {
|
|
799
|
+
for (const [turnIndex, turn] of section.turns.entries()) {
|
|
800
|
+
// Collect this turn's events by walking the source in order; the sections
|
|
801
|
+
// were built from the same array, so the cursor never rewinds.
|
|
802
|
+
const collected = [];
|
|
803
|
+
while (cursor < source.length) {
|
|
804
|
+
const event = source[cursor];
|
|
805
|
+
cursor += 1;
|
|
806
|
+
if (event === undefined)
|
|
807
|
+
break;
|
|
808
|
+
if (event.type === 'turn/start') {
|
|
809
|
+
const turnNumber = turnOf(event);
|
|
810
|
+
if (turnNumber !== undefined)
|
|
811
|
+
lastTurn = turnNumber;
|
|
812
|
+
collected.push(event);
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
collected.push(event);
|
|
816
|
+
if (event.type === 'user/message' || event.type === 'assistant/message') {
|
|
817
|
+
if (!isInjection(event) && !isCheckpoint(event))
|
|
818
|
+
break;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
slots.push({ key: turnKey(sectionIndex, turnIndex), turn: turn.turn, events: collected });
|
|
790
822
|
}
|
|
791
|
-
const turn = turnOf(event) ?? lastTurn;
|
|
792
|
-
if (turn === undefined)
|
|
793
|
-
continue;
|
|
794
|
-
const bucket = turns.get(turn);
|
|
795
|
-
if (bucket === undefined)
|
|
796
|
-
turns.set(turn, [event]);
|
|
797
|
-
else
|
|
798
|
-
bucket.push(event);
|
|
799
823
|
}
|
|
800
|
-
const ordered = [...turns.entries()].sort((left, right) => left[0] - right[0]);
|
|
801
824
|
const extracted = new Map();
|
|
802
|
-
for (let index = 0; index <
|
|
825
|
+
for (let index = 0; index < slots.length; index += TURNS_PER_CALL) {
|
|
803
826
|
if (signal?.aborted === true)
|
|
804
827
|
break;
|
|
805
|
-
const batch =
|
|
828
|
+
const batch = slots.slice(index, index + TURNS_PER_CALL);
|
|
806
829
|
const blocks = await extractBatch(this.ctx, agent, batch, signal);
|
|
807
|
-
for (const [
|
|
808
|
-
extracted.set(
|
|
830
|
+
for (const [key, block] of blocks)
|
|
831
|
+
extracted.set(key, block);
|
|
809
832
|
}
|
|
810
833
|
return extracted;
|
|
811
834
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -156,7 +156,7 @@ export declare function buildSections(events: readonly SessionEvent[]): Section[
|
|
|
156
156
|
* ceiling; the tiers exist for the turns where extraction was unavailable and
|
|
157
157
|
* the prose is all that remains.
|
|
158
158
|
*/
|
|
159
|
-
export declare function renderDigest(events: readonly SessionEvent[], budgetTokens: number, extracted?: ReadonlyMap<
|
|
159
|
+
export declare function renderDigest(events: readonly SessionEvent[], budgetTokens: number, extracted?: ReadonlyMap<string, string>): string;
|
|
160
160
|
/**
|
|
161
161
|
* The surface seqs this compaction shadows, recovered from the summarization
|
|
162
162
|
* input's message count.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logictan/dsh-verbatim-compaction",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Deterministic compaction backend that keeps conversation prose verbatim in the checkpoint instead of re-summarizing it: user and assistant text survive word for word, tool results collapse to one-line pointers, reasoning is never read, and no model call is made.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|