open-multi-agent-kit 0.80.4 → 0.80.6
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/CHANGELOG.md +21 -0
- package/dist/config.d.ts +4 -9
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +46 -15
- package/dist/config.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +6 -1
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +82 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +606 -10
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/harness-control-events.d.ts +79 -0
- package/dist/core/harness-control-events.d.ts.map +1 -0
- package/dist/core/harness-control-events.js +339 -0
- package/dist/core/harness-control-events.js.map +1 -0
- package/dist/core/harness-control-replay.d.ts +26 -0
- package/dist/core/harness-control-replay.d.ts.map +1 -0
- package/dist/core/harness-control-replay.js +155 -0
- package/dist/core/harness-control-replay.js.map +1 -0
- package/dist/core/keybindings.d.ts +12 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +31 -1
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/mcp-inventory.d.ts +40 -0
- package/dist/core/mcp-inventory.d.ts.map +1 -0
- package/dist/core/mcp-inventory.js +104 -0
- package/dist/core/mcp-inventory.js.map +1 -0
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +4 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +4 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/spec-kit/compiler.d.ts +67 -0
- package/dist/core/spec-kit/compiler.d.ts.map +1 -0
- package/dist/core/spec-kit/compiler.js +268 -0
- package/dist/core/spec-kit/compiler.js.map +1 -0
- package/dist/core/transaction-coordinator.d.ts +30 -0
- package/dist/core/transaction-coordinator.d.ts.map +1 -0
- package/dist/core/transaction-coordinator.js +173 -0
- package/dist/core/transaction-coordinator.js.map +1 -0
- package/dist/migrations.d.ts +35 -0
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +409 -3
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/control-panel.d.ts +29 -0
- package/dist/modes/interactive/components/control-panel.d.ts.map +1 -0
- package/dist/modes/interactive/components/control-panel.js +90 -0
- package/dist/modes/interactive/components/control-panel.js.map +1 -0
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +21 -13
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +3 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +3 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/mcp-selector.d.ts +28 -0
- package/dist/modes/interactive/components/mcp-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/mcp-selector.js +138 -0
- package/dist/modes/interactive/components/mcp-selector.js.map +1 -0
- package/dist/modes/interactive/components/model-selector.d.ts +12 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +120 -27
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +19 -1
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +21 -8
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/skills-selector.d.ts +32 -0
- package/dist/modes/interactive/components/skills-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/skills-selector.js +130 -0
- package/dist/modes/interactive/components/skills-selector.js.map +1 -0
- package/dist/modes/interactive/components/thinking-selector.d.ts +7 -8
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +7 -48
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +9 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +340 -97
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/catppuccin-mocha.json +86 -0
- package/dist/modes/interactive/theme/dracula.json +86 -0
- package/dist/modes/interactive/theme/gruvbox-dark.json +86 -0
- package/dist/modes/interactive/theme/kanagawa-dragon.json +86 -0
- package/dist/modes/interactive/theme/omk-control-panel.json +91 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +41 -16
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/interactive/theme/tokyo-night-storm.json +86 -0
- package/docs/compaction.md +21 -8
- package/docs/quickstart.md +1 -1
- package/docs/themes.md +10 -2
- package/docs/usage.md +2 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* and after compaction the session is reloaded.
|
|
6
6
|
*/
|
|
7
7
|
import { completeSimple } from "@earendil-works/omk-ai";
|
|
8
|
+
import { recordHarnessControlEvent } from "../harness-control-events.js";
|
|
8
9
|
import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
|
|
9
10
|
import { buildSessionContext } from "../session-manager.js";
|
|
10
11
|
import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, SUMMARIZATION_SYSTEM_PROMPT, serializeConversation, } from "./utils.js";
|
|
@@ -64,11 +65,13 @@ function getMessageFromEntryForCompaction(entry) {
|
|
|
64
65
|
return getMessageFromEntry(entry);
|
|
65
66
|
}
|
|
66
67
|
export const DEFAULT_COMPACTION_MAX_USAGE_RATIO = 0.9;
|
|
68
|
+
export const DEFAULT_COMPACTION_SUMMARY_INPUT_TOKENS = 80000;
|
|
67
69
|
export const DEFAULT_COMPACTION_SETTINGS = {
|
|
68
70
|
enabled: true,
|
|
69
71
|
reserveTokens: 16384,
|
|
70
72
|
keepRecentTokens: 20000,
|
|
71
73
|
maxUsageRatio: DEFAULT_COMPACTION_MAX_USAGE_RATIO,
|
|
74
|
+
summaryInputTokens: DEFAULT_COMPACTION_SUMMARY_INPUT_TOKENS,
|
|
72
75
|
};
|
|
73
76
|
// ============================================================================
|
|
74
77
|
// Token calculation
|
|
@@ -374,6 +377,401 @@ export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
|
|
|
374
377
|
};
|
|
375
378
|
}
|
|
376
379
|
// ============================================================================
|
|
380
|
+
// Summarization Input Packing
|
|
381
|
+
// ============================================================================
|
|
382
|
+
const CHARS_PER_TOKEN_ESTIMATE = 4;
|
|
383
|
+
const SUMMARY_INPUT_PROMPT_OVERHEAD_TOKENS = 2048;
|
|
384
|
+
const SUMMARY_INPUT_TOKENIZER_SAFETY_MARGIN = 512;
|
|
385
|
+
const MIN_SUMMARY_INPUT_TOKENS = 512;
|
|
386
|
+
function estimateTextTokens(text) {
|
|
387
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN_ESTIMATE);
|
|
388
|
+
}
|
|
389
|
+
function isFinitePositiveInteger(value) {
|
|
390
|
+
return value !== undefined && Number.isFinite(value) && value > 0;
|
|
391
|
+
}
|
|
392
|
+
export function resolveSummaryInputTokenBudgetV3(model, maxOutputTokens, options) {
|
|
393
|
+
const configuredSummaryInputTokens = typeof options === "number" ? options : options?.configuredSummaryInputTokens;
|
|
394
|
+
if (configuredSummaryInputTokens !== undefined && configuredSummaryInputTokens <= 0) {
|
|
395
|
+
return { emergency: false, fixedPromptTokens: 0 };
|
|
396
|
+
}
|
|
397
|
+
const configuredBudget = configuredSummaryInputTokens ?? DEFAULT_COMPACTION_SUMMARY_INPUT_TOKENS;
|
|
398
|
+
if (!isFinitePositiveInteger(configuredBudget)) {
|
|
399
|
+
return { emergency: false, fixedPromptTokens: 0 };
|
|
400
|
+
}
|
|
401
|
+
const contextWindow = model.contextWindow > 0 ? Math.floor(model.contextWindow) : Number.POSITIVE_INFINITY;
|
|
402
|
+
if (!Number.isFinite(contextWindow)) {
|
|
403
|
+
return { budgetTokens: Math.floor(configuredBudget), emergency: false, fixedPromptTokens: 0 };
|
|
404
|
+
}
|
|
405
|
+
const fixedPromptTokens = typeof options === "object"
|
|
406
|
+
? estimateTextTokens([
|
|
407
|
+
options.systemPrompt,
|
|
408
|
+
options.basePrompt,
|
|
409
|
+
options.customInstructions,
|
|
410
|
+
options.previousSummary,
|
|
411
|
+
options.wrapperText,
|
|
412
|
+
]
|
|
413
|
+
.filter((value) => typeof value === "string" && value.length > 0)
|
|
414
|
+
.join("\n"))
|
|
415
|
+
: SUMMARY_INPUT_PROMPT_OVERHEAD_TOKENS;
|
|
416
|
+
const safetyMarginTokens = typeof options === "object" ? (options.safetyMarginTokens ?? SUMMARY_INPUT_TOKENIZER_SAFETY_MARGIN) : 0;
|
|
417
|
+
const availableInputTokens = contextWindow - Math.max(0, Math.floor(maxOutputTokens)) - fixedPromptTokens - safetyMarginTokens;
|
|
418
|
+
if (availableInputTokens < MIN_SUMMARY_INPUT_TOKENS) {
|
|
419
|
+
return {
|
|
420
|
+
emergency: true,
|
|
421
|
+
reason: "fixed prompt overhead exceeds viable summarization input budget",
|
|
422
|
+
fixedPromptTokens,
|
|
423
|
+
availableInputTokens,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
budgetTokens: Math.max(MIN_SUMMARY_INPUT_TOKENS, Math.min(Math.floor(configuredBudget), availableInputTokens)),
|
|
428
|
+
emergency: false,
|
|
429
|
+
fixedPromptTokens,
|
|
430
|
+
availableInputTokens,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
export function resolveSummaryInputTokenBudget(model, maxOutputTokens, options) {
|
|
434
|
+
return resolveSummaryInputTokenBudgetV3(model, maxOutputTokens, options).budgetTokens;
|
|
435
|
+
}
|
|
436
|
+
function scoreSummaryLine(line) {
|
|
437
|
+
const trimmed = line.trim();
|
|
438
|
+
if (trimmed.length === 0)
|
|
439
|
+
return 0;
|
|
440
|
+
let score = 0;
|
|
441
|
+
if (/\b(?:[\w.-]+\/)+[\w.-]+\.(?:ts|tsx|js|jsx|json|md|css|scss|py|rs|go|java|kt|swift|sh|yml|yaml|toml)\b/.test(trimmed)) {
|
|
442
|
+
score += 6;
|
|
443
|
+
}
|
|
444
|
+
if (/\b(?:error|failed|failure|exception|warning|blocked|regression|timeout|overflow|abort)\b/i.test(trimmed) ||
|
|
445
|
+
/(?:오류|실패|경고|차단|회귀|시간 초과|타임아웃)/.test(trimmed)) {
|
|
446
|
+
score += 5;
|
|
447
|
+
}
|
|
448
|
+
if (/\b(?:npm|node|pnpm|yarn|bun|git|gh|tsgo|vitest|biome|pytest|cargo|go test)\b/.test(trimmed)) {
|
|
449
|
+
score += 4;
|
|
450
|
+
}
|
|
451
|
+
if (/^(?:#{1,6}\s|[-*]\s|\d+\.\s|\[[ xX]\]|```)/.test(trimmed)) {
|
|
452
|
+
score += 3;
|
|
453
|
+
}
|
|
454
|
+
if (/\b(?:goal|constraint|decision|next step|todo|changed|created|modified|verified|passed|failed)\b/i.test(trimmed) ||
|
|
455
|
+
/(?:목표|제약|결정|다음 단계|검증|통과|수정|생성|변경|증거)/.test(trimmed)) {
|
|
456
|
+
score += 3;
|
|
457
|
+
}
|
|
458
|
+
if (trimmed.length <= 220) {
|
|
459
|
+
score += 1;
|
|
460
|
+
}
|
|
461
|
+
else if (trimmed.length > 1000) {
|
|
462
|
+
score -= 3;
|
|
463
|
+
}
|
|
464
|
+
return score;
|
|
465
|
+
}
|
|
466
|
+
function selectHighSignalLines(text, budgetChars) {
|
|
467
|
+
if (budgetChars <= 0)
|
|
468
|
+
return "";
|
|
469
|
+
const scoredLines = text
|
|
470
|
+
.split("\n")
|
|
471
|
+
.map((line, index) => ({ index, line, score: scoreSummaryLine(line) }))
|
|
472
|
+
.filter((item) => item.score > 0)
|
|
473
|
+
.sort((a, b) => b.score - a.score || a.index - b.index);
|
|
474
|
+
const selected = [];
|
|
475
|
+
let usedChars = 0;
|
|
476
|
+
for (const item of scoredLines) {
|
|
477
|
+
const lineChars = item.line.length + 1;
|
|
478
|
+
if (usedChars + lineChars > budgetChars)
|
|
479
|
+
continue;
|
|
480
|
+
selected.push({ index: item.index, line: item.line });
|
|
481
|
+
usedChars += lineChars;
|
|
482
|
+
}
|
|
483
|
+
if (selected.length === 0) {
|
|
484
|
+
return text.slice(0, budgetChars);
|
|
485
|
+
}
|
|
486
|
+
return selected
|
|
487
|
+
.sort((a, b) => a.index - b.index)
|
|
488
|
+
.map((item) => item.line)
|
|
489
|
+
.join("\n");
|
|
490
|
+
}
|
|
491
|
+
function scoreSummaryUnit(unit) {
|
|
492
|
+
const lineScores = unit.split("\n").map(scoreSummaryLine);
|
|
493
|
+
const maxLineScore = Math.max(0, ...lineScores);
|
|
494
|
+
const aggregateScore = lineScores.reduce((sum, score) => sum + Math.max(0, score), 0);
|
|
495
|
+
let score = maxLineScore * 2 + Math.min(aggregateScore, 12);
|
|
496
|
+
if (/^```/m.test(unit))
|
|
497
|
+
score += 3;
|
|
498
|
+
if (/^#{1,6}\s/m.test(unit))
|
|
499
|
+
score += 2;
|
|
500
|
+
if (unit.length > 2400)
|
|
501
|
+
score -= 4;
|
|
502
|
+
return score;
|
|
503
|
+
}
|
|
504
|
+
function extractSummaryUnitSignals(text) {
|
|
505
|
+
const signals = new Set();
|
|
506
|
+
for (const match of text.matchAll(/\b(?:[\w.-]+\/)+[\w.-]+\.(?:ts|tsx|js|jsx|json|md|css|scss|py|rs|go|java|kt|swift|sh|yml|yaml|toml)\b/g)) {
|
|
507
|
+
signals.add(`path:${match[0]}`);
|
|
508
|
+
}
|
|
509
|
+
for (const match of text.matchAll(/\b(?:npm|node|pnpm|yarn|bun|git|gh|tsgo|vitest|biome|pytest|cargo|go test)\b[^\n]*/g)) {
|
|
510
|
+
signals.add(`cmd:${match[0].trim().slice(0, 160)}`);
|
|
511
|
+
}
|
|
512
|
+
for (const match of text.matchAll(/\b(?:HCP-\d+|R\d+)\b/g)) {
|
|
513
|
+
signals.add(`id:${match[0]}`);
|
|
514
|
+
}
|
|
515
|
+
for (const match of text.matchAll(/\.omk\/[\w./-]+/g)) {
|
|
516
|
+
signals.add(`evidence:${match[0]}`);
|
|
517
|
+
}
|
|
518
|
+
return [...signals];
|
|
519
|
+
}
|
|
520
|
+
function updateHeadingStack(headingStack, headingLine) {
|
|
521
|
+
const match = headingLine.match(/^(#{1,6})\s+(.+)$/);
|
|
522
|
+
if (!match)
|
|
523
|
+
return headingStack;
|
|
524
|
+
const level = match[1].length;
|
|
525
|
+
const title = match[2].replace(/\s+#+\s*$/, "").trim();
|
|
526
|
+
const nextStack = [...headingStack];
|
|
527
|
+
while (nextStack.length > 0 && nextStack.at(-1).level >= level) {
|
|
528
|
+
nextStack.pop();
|
|
529
|
+
}
|
|
530
|
+
nextStack.push({ level, title });
|
|
531
|
+
return nextStack;
|
|
532
|
+
}
|
|
533
|
+
function inferSummaryUnitStatus(text) {
|
|
534
|
+
if (/^- \[[xX]]/m.test(text) || /\b(?:completed|done|fixed)\b/i.test(text) || /(?:완료|수정됨)/.test(text)) {
|
|
535
|
+
return "done";
|
|
536
|
+
}
|
|
537
|
+
if (/^- \[ ]/m.test(text) || /\b(?:in progress|todo|next)\b/i.test(text) || /(?:진행|다음)/.test(text)) {
|
|
538
|
+
return "in_progress";
|
|
539
|
+
}
|
|
540
|
+
if (/\b(?:blocked|blocker)\b/i.test(text) || /(?:차단|블로커)/.test(text))
|
|
541
|
+
return "blocked";
|
|
542
|
+
if (/\b(?:failed|failure|error|exception|regression)\b/i.test(text) || /(?:실패|오류|회귀)/.test(text)) {
|
|
543
|
+
return "failed";
|
|
544
|
+
}
|
|
545
|
+
if (/\b(?:passed|success|ok)\b/i.test(text) || /(?:통과|성공)/.test(text))
|
|
546
|
+
return "passed";
|
|
547
|
+
return undefined;
|
|
548
|
+
}
|
|
549
|
+
function inferSummaryUnitKind(text, status) {
|
|
550
|
+
const trimmed = text.trim();
|
|
551
|
+
if (status === "blocked")
|
|
552
|
+
return "blocker";
|
|
553
|
+
if (trimmed.startsWith("```"))
|
|
554
|
+
return "code";
|
|
555
|
+
if (/\.omk\/[\w./-]+|\bevidence\b/i.test(text))
|
|
556
|
+
return "evidence";
|
|
557
|
+
if (/\b(?:npm|node|pnpm|yarn|bun|git|gh|tsgo|vitest|biome|pytest|cargo|go test)\b/.test(text))
|
|
558
|
+
return "command";
|
|
559
|
+
if (/^(?:[-*]\s+\[[ xX]])/m.test(text))
|
|
560
|
+
return "checklist";
|
|
561
|
+
if (/^#{1,6}\s/m.test(text))
|
|
562
|
+
return "heading";
|
|
563
|
+
return "text";
|
|
564
|
+
}
|
|
565
|
+
function createSummaryInputSemanticUnit(index, text, sectionPath) {
|
|
566
|
+
const status = inferSummaryUnitStatus(text);
|
|
567
|
+
return {
|
|
568
|
+
index,
|
|
569
|
+
text,
|
|
570
|
+
score: scoreSummaryUnit(text),
|
|
571
|
+
sectionPath,
|
|
572
|
+
kind: inferSummaryUnitKind(text, status),
|
|
573
|
+
refs: extractSummaryUnitSignals(text),
|
|
574
|
+
status,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
export function splitSummaryInputSemanticUnits(text) {
|
|
578
|
+
const lines = text.split("\n");
|
|
579
|
+
const units = [];
|
|
580
|
+
let start = 0;
|
|
581
|
+
let current = [];
|
|
582
|
+
let inFence = false;
|
|
583
|
+
let activeHeadingStack = [];
|
|
584
|
+
let currentSectionPath = [];
|
|
585
|
+
function flush(nextIndex) {
|
|
586
|
+
while (current.length > 0 && current[0]?.trim() === "") {
|
|
587
|
+
current.shift();
|
|
588
|
+
start++;
|
|
589
|
+
}
|
|
590
|
+
while (current.length > 0 && current.at(-1)?.trim() === "") {
|
|
591
|
+
current.pop();
|
|
592
|
+
}
|
|
593
|
+
if (current.length === 0) {
|
|
594
|
+
start = nextIndex;
|
|
595
|
+
currentSectionPath = activeHeadingStack.map((heading) => heading.title);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const unitText = current.join("\n");
|
|
599
|
+
units.push(createSummaryInputSemanticUnit(start, unitText, [...currentSectionPath]));
|
|
600
|
+
current = [];
|
|
601
|
+
start = nextIndex;
|
|
602
|
+
currentSectionPath = activeHeadingStack.map((heading) => heading.title);
|
|
603
|
+
}
|
|
604
|
+
for (let index = 0; index < lines.length; index++) {
|
|
605
|
+
const line = lines[index];
|
|
606
|
+
const trimmed = line.trim();
|
|
607
|
+
const startsFence = trimmed.startsWith("```");
|
|
608
|
+
const startsHeading = /^#{1,6}\s/.test(trimmed);
|
|
609
|
+
if (!inFence && startsHeading && current.some((entry) => entry.trim().length > 0)) {
|
|
610
|
+
flush(index);
|
|
611
|
+
}
|
|
612
|
+
if (!inFence && startsHeading) {
|
|
613
|
+
activeHeadingStack = updateHeadingStack(activeHeadingStack, trimmed);
|
|
614
|
+
}
|
|
615
|
+
if (current.length === 0) {
|
|
616
|
+
start = index;
|
|
617
|
+
currentSectionPath = activeHeadingStack.map((heading) => heading.title);
|
|
618
|
+
}
|
|
619
|
+
current.push(line);
|
|
620
|
+
if (startsFence) {
|
|
621
|
+
inFence = !inFence;
|
|
622
|
+
if (!inFence)
|
|
623
|
+
flush(index + 1);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
if (!inFence && trimmed.length === 0) {
|
|
627
|
+
flush(index + 1);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
flush(lines.length);
|
|
631
|
+
return units;
|
|
632
|
+
}
|
|
633
|
+
export function buildSummaryDependencyGraph(units) {
|
|
634
|
+
const edges = [];
|
|
635
|
+
const signalsByIndex = new Map();
|
|
636
|
+
for (const unit of units) {
|
|
637
|
+
signalsByIndex.set(unit.index, new Set(unit.refs));
|
|
638
|
+
}
|
|
639
|
+
for (let i = 0; i < units.length; i++) {
|
|
640
|
+
for (let j = i + 1; j < units.length; j++) {
|
|
641
|
+
const leftUnit = units[i];
|
|
642
|
+
const rightUnit = units[j];
|
|
643
|
+
const left = signalsByIndex.get(leftUnit.index) ?? new Set();
|
|
644
|
+
const right = signalsByIndex.get(rightUnit.index) ?? new Set();
|
|
645
|
+
const shared = [...left].find((signal) => right.has(signal));
|
|
646
|
+
const sameSection = leftUnit.sectionPath.length > 0 && leftUnit.sectionPath.join("/") === rightUnit.sectionPath.join("/");
|
|
647
|
+
if (shared || sameSection) {
|
|
648
|
+
const reason = shared ?? `section:${leftUnit.sectionPath.join("/")}`;
|
|
649
|
+
edges.push({ from: leftUnit.index, to: rightUnit.index, reason });
|
|
650
|
+
edges.push({ from: rightUnit.index, to: leftUnit.index, reason });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return { nodes: units, edges };
|
|
655
|
+
}
|
|
656
|
+
export function selectSummaryDependencyClosure(graph, seedIndexes, maxAdditionalNodes = 8) {
|
|
657
|
+
const selected = new Set(seedIndexes);
|
|
658
|
+
const queue = [...seedIndexes];
|
|
659
|
+
while (queue.length > 0 && selected.size < seedIndexes.length + maxAdditionalNodes) {
|
|
660
|
+
const index = queue.shift();
|
|
661
|
+
const neighbors = graph.edges
|
|
662
|
+
.filter((edge) => edge.from === index)
|
|
663
|
+
.map((edge) => edge.to)
|
|
664
|
+
.filter((neighbor) => !selected.has(neighbor));
|
|
665
|
+
for (const neighbor of neighbors) {
|
|
666
|
+
selected.add(neighbor);
|
|
667
|
+
queue.push(neighbor);
|
|
668
|
+
if (selected.size >= seedIndexes.length + maxAdditionalNodes)
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return selected;
|
|
673
|
+
}
|
|
674
|
+
function normalizeSummaryNovelty(text) {
|
|
675
|
+
return text
|
|
676
|
+
.toLowerCase()
|
|
677
|
+
.replace(/\b\d+\b/g, "#")
|
|
678
|
+
.replace(/\s+/g, " ")
|
|
679
|
+
.trim();
|
|
680
|
+
}
|
|
681
|
+
export function deduplicateSummaryFactsByNovelty(units) {
|
|
682
|
+
const seen = new Set();
|
|
683
|
+
const result = [];
|
|
684
|
+
for (const unit of units) {
|
|
685
|
+
const sectionKey = unit.sectionPath.join("/");
|
|
686
|
+
const key = [sectionKey, unit.kind, unit.status ?? "", normalizeSummaryNovelty(unit.text)].join("|");
|
|
687
|
+
if (seen.has(key))
|
|
688
|
+
continue;
|
|
689
|
+
seen.add(key);
|
|
690
|
+
result.push(unit);
|
|
691
|
+
}
|
|
692
|
+
return result;
|
|
693
|
+
}
|
|
694
|
+
function selectHighSignalSemanticUnits(text, budgetChars) {
|
|
695
|
+
if (budgetChars <= 0)
|
|
696
|
+
return "";
|
|
697
|
+
const candidateUnits = deduplicateSummaryFactsByNovelty(splitSummaryInputSemanticUnits(text).filter((unit) => unit.score > 0));
|
|
698
|
+
const graph = buildSummaryDependencyGraph(candidateUnits);
|
|
699
|
+
const rankedUnits = [...candidateUnits].sort((a, b) => b.score - a.score || a.index - b.index);
|
|
700
|
+
const seedIndexes = rankedUnits.slice(0, Math.min(12, rankedUnits.length)).map((unit) => unit.index);
|
|
701
|
+
const closureIndexes = selectSummaryDependencyClosure(graph, seedIndexes);
|
|
702
|
+
const units = rankedUnits
|
|
703
|
+
.filter((unit) => closureIndexes.has(unit.index))
|
|
704
|
+
.sort((a, b) => b.score - a.score || a.index - b.index);
|
|
705
|
+
const selected = [];
|
|
706
|
+
let usedChars = 0;
|
|
707
|
+
for (const unit of units) {
|
|
708
|
+
const unitChars = unit.text.length + 2;
|
|
709
|
+
if (unitChars > budgetChars) {
|
|
710
|
+
const excerpt = selectHighSignalLines(unit.text, Math.max(0, budgetChars - usedChars));
|
|
711
|
+
if (excerpt.length > 0) {
|
|
712
|
+
selected.push({ ...unit, text: excerpt });
|
|
713
|
+
usedChars += excerpt.length + 2;
|
|
714
|
+
}
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (usedChars + unitChars > budgetChars)
|
|
718
|
+
continue;
|
|
719
|
+
selected.push(unit);
|
|
720
|
+
usedChars += unitChars;
|
|
721
|
+
}
|
|
722
|
+
if (selected.length === 0) {
|
|
723
|
+
return text.slice(0, budgetChars);
|
|
724
|
+
}
|
|
725
|
+
return selected
|
|
726
|
+
.sort((a, b) => a.index - b.index)
|
|
727
|
+
.map((unit) => unit.text)
|
|
728
|
+
.join("\n\n");
|
|
729
|
+
}
|
|
730
|
+
function trimToCharBudget(text, budgetChars, fromEnd = false) {
|
|
731
|
+
if (budgetChars <= 0)
|
|
732
|
+
return "";
|
|
733
|
+
if (text.length <= budgetChars)
|
|
734
|
+
return text;
|
|
735
|
+
return fromEnd ? text.slice(text.length - budgetChars) : text.slice(0, budgetChars);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Pack serialized conversation text under a token budget using a deterministic
|
|
739
|
+
* extractive strategy: preserve the opening, recent tail, and high-signal
|
|
740
|
+
* middle lines such as paths, commands, errors, and decisions.
|
|
741
|
+
*/
|
|
742
|
+
export function packSummaryInputForTokenBudget(text, maxInputTokens) {
|
|
743
|
+
const originalTokens = estimateTextTokens(text);
|
|
744
|
+
if (!isFinitePositiveInteger(maxInputTokens) || originalTokens <= maxInputTokens) {
|
|
745
|
+
return {
|
|
746
|
+
text,
|
|
747
|
+
originalTokens,
|
|
748
|
+
packedTokens: originalTokens,
|
|
749
|
+
omittedTokens: 0,
|
|
750
|
+
wasCompressed: false,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
const targetChars = Math.max(MIN_SUMMARY_INPUT_TOKENS * CHARS_PER_TOKEN_ESTIMATE, Math.floor(maxInputTokens) * 4);
|
|
754
|
+
const marker = `<omk-summary-input-compressed originalTokens="${originalTokens}" targetTokens="${Math.floor(maxInputTokens)}">\nOlder serialized history was packed before summarization. Opening context, high-signal semantic units, and the recent tail were retained; low-signal repeated text was omitted.\n</omk-summary-input-compressed>`;
|
|
755
|
+
const contentBudgetChars = Math.max(512, targetChars - marker.length - 64);
|
|
756
|
+
const headBudgetChars = Math.floor(contentBudgetChars * 0.18);
|
|
757
|
+
const tailBudgetChars = Math.floor(contentBudgetChars * 0.52);
|
|
758
|
+
const middleBudgetChars = Math.max(0, contentBudgetChars - headBudgetChars - tailBudgetChars);
|
|
759
|
+
const head = trimToCharBudget(text, headBudgetChars);
|
|
760
|
+
const tail = trimToCharBudget(text, tailBudgetChars, true);
|
|
761
|
+
const middleStart = Math.min(headBudgetChars, text.length);
|
|
762
|
+
const middleEnd = Math.max(middleStart, text.length - tailBudgetChars);
|
|
763
|
+
const middle = selectHighSignalSemanticUnits(text.slice(middleStart, middleEnd), middleBudgetChars);
|
|
764
|
+
const packedText = [head.trimEnd(), marker, middle.trim(), tail.trimStart()].filter(Boolean).join("\n\n");
|
|
765
|
+
const packedTokens = estimateTextTokens(packedText);
|
|
766
|
+
return {
|
|
767
|
+
text: packedText,
|
|
768
|
+
originalTokens,
|
|
769
|
+
packedTokens,
|
|
770
|
+
omittedTokens: Math.max(0, originalTokens - packedTokens),
|
|
771
|
+
wasCompressed: true,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
// ============================================================================
|
|
377
775
|
// Summarization
|
|
378
776
|
// ============================================================================
|
|
379
777
|
const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.
|
|
@@ -407,7 +805,12 @@ Use this EXACT format:
|
|
|
407
805
|
- [Any data, examples, or references needed to continue]
|
|
408
806
|
- [Or "(none)" if not applicable]
|
|
409
807
|
|
|
410
|
-
|
|
808
|
+
## Resume Handoff
|
|
809
|
+
- **First action**: [Single concrete next action after compaction]
|
|
810
|
+
- **Re-check before editing**: [Workspace state, files, or commands to verify first; use "(none)" if not needed]
|
|
811
|
+
- **Evidence status**: [Checks/tests already run and results, or "(not run)"]
|
|
812
|
+
|
|
813
|
+
Keep each section concise. Preserve exact file paths, function names, command results, and error messages.`;
|
|
411
814
|
const UPDATE_SUMMARIZATION_PROMPT = `The messages above are NEW conversation messages to incorporate into the existing summary provided in <previous-summary> tags.
|
|
412
815
|
|
|
413
816
|
Update the existing structured summary with new information. RULES:
|
|
@@ -445,7 +848,150 @@ Use this EXACT format:
|
|
|
445
848
|
## Critical Context
|
|
446
849
|
- [Preserve important context, add new if needed]
|
|
447
850
|
|
|
448
|
-
|
|
851
|
+
## Resume Handoff
|
|
852
|
+
- **First action**: [Single concrete next action after compaction]
|
|
853
|
+
- **Re-check before editing**: [Workspace state, files, or commands to verify first; use "(none)" if not needed]
|
|
854
|
+
- **Evidence status**: [Checks/tests already run and results, or "(not run)"]
|
|
855
|
+
|
|
856
|
+
Keep each section concise. Preserve exact file paths, function names, command results, and error messages.`;
|
|
857
|
+
const REQUIRED_SUMMARY_SECTIONS = [
|
|
858
|
+
"## Goal",
|
|
859
|
+
"## Constraints & Preferences",
|
|
860
|
+
"## Progress",
|
|
861
|
+
"### Done",
|
|
862
|
+
"### In Progress",
|
|
863
|
+
"### Blocked",
|
|
864
|
+
"## Key Decisions",
|
|
865
|
+
"## Next Steps",
|
|
866
|
+
"## Critical Context",
|
|
867
|
+
"## Resume Handoff",
|
|
868
|
+
];
|
|
869
|
+
export function parseCompactionSummary(summary) {
|
|
870
|
+
const sections = {};
|
|
871
|
+
for (const heading of REQUIRED_SUMMARY_SECTIONS) {
|
|
872
|
+
const value = extractSection(summary, heading);
|
|
873
|
+
if (value !== undefined)
|
|
874
|
+
sections[heading] = value;
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
sections,
|
|
878
|
+
missingSections: REQUIRED_SUMMARY_SECTIONS.filter((heading) => sections[heading] === undefined),
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
function extractSection(summary, heading) {
|
|
882
|
+
const start = summary.indexOf(heading);
|
|
883
|
+
if (start < 0)
|
|
884
|
+
return undefined;
|
|
885
|
+
const nextHeading = summary.slice(start + heading.length).search(/\n#{2,3}\s/);
|
|
886
|
+
if (nextHeading < 0)
|
|
887
|
+
return summary.slice(start + heading.length).trim();
|
|
888
|
+
return summary.slice(start + heading.length, start + heading.length + nextHeading).trim();
|
|
889
|
+
}
|
|
890
|
+
function extractCriticalFactsFromText(text, limit = 12) {
|
|
891
|
+
return text
|
|
892
|
+
.split("\n")
|
|
893
|
+
.map((line) => line.trim())
|
|
894
|
+
.filter((line) => line.length > 0 && scoreSummaryLine(line) >= 4)
|
|
895
|
+
.slice(0, limit);
|
|
896
|
+
}
|
|
897
|
+
export function createEmergencyCompactionHandoff(options) {
|
|
898
|
+
const facts = options.fallbackFacts ?? extractCriticalFactsFromText(options.conversationText, 16);
|
|
899
|
+
const previousBlocked = options.previousSummary
|
|
900
|
+
? (extractSection(options.previousSummary, "### Blocked") ??
|
|
901
|
+
extractSection(options.previousSummary, "## Blocked"))
|
|
902
|
+
: undefined;
|
|
903
|
+
return [
|
|
904
|
+
"## Goal",
|
|
905
|
+
"Deterministic emergency compaction handoff because normal summarization could exceed context budget.",
|
|
906
|
+
"",
|
|
907
|
+
"## Constraints & Preferences",
|
|
908
|
+
`- Emergency reason: ${options.reason}`,
|
|
909
|
+
"- Preserve exact paths, commands, blockers, and evidence before continuing.",
|
|
910
|
+
"",
|
|
911
|
+
"## Progress",
|
|
912
|
+
"### Done",
|
|
913
|
+
"- [x] Created emergency handoff without calling the summarization model.",
|
|
914
|
+
"",
|
|
915
|
+
"### In Progress",
|
|
916
|
+
"- [ ] Re-check workspace state and continue from critical facts.",
|
|
917
|
+
"",
|
|
918
|
+
"### Blocked",
|
|
919
|
+
previousBlocked && previousBlocked.length > 0 ? previousBlocked : "- (none)",
|
|
920
|
+
"",
|
|
921
|
+
"## Key Decisions",
|
|
922
|
+
"- **Emergency handoff**: Avoided forced minimum input budget that could overflow the model context.",
|
|
923
|
+
"",
|
|
924
|
+
"## Next Steps",
|
|
925
|
+
"1. Re-check workspace state before editing.",
|
|
926
|
+
"2. Resume from Critical Context and run verification before completion.",
|
|
927
|
+
"",
|
|
928
|
+
"## Critical Context",
|
|
929
|
+
formatFallbackFacts(facts),
|
|
930
|
+
"",
|
|
931
|
+
"## Resume Handoff",
|
|
932
|
+
"- **First action**: Re-check workspace state and relevant files from Critical Context.",
|
|
933
|
+
"- **Re-check before editing**: git status and target file contents.",
|
|
934
|
+
"- **Evidence status**: emergency handoff generated; downstream checks not run by compaction.",
|
|
935
|
+
].join("\n");
|
|
936
|
+
}
|
|
937
|
+
function formatFallbackFacts(fallbackFacts) {
|
|
938
|
+
if (!fallbackFacts || fallbackFacts.length === 0)
|
|
939
|
+
return "- (not captured)";
|
|
940
|
+
return fallbackFacts.map((fact) => `- ${fact}`).join("\n");
|
|
941
|
+
}
|
|
942
|
+
export function validateCompactionSummaryContract(summary, options = {}) {
|
|
943
|
+
const trimmedSummary = summary.trim();
|
|
944
|
+
const missingSections = REQUIRED_SUMMARY_SECTIONS.filter((section) => !trimmedSummary.includes(section));
|
|
945
|
+
const previousBlocked = options.previousSummary
|
|
946
|
+
? (extractSection(options.previousSummary, "### Blocked") ??
|
|
947
|
+
extractSection(options.previousSummary, "## Blocked"))
|
|
948
|
+
: undefined;
|
|
949
|
+
const shouldPreservePreviousBlocked = previousBlocked !== undefined && previousBlocked.length > 0 && !trimmedSummary.includes(previousBlocked);
|
|
950
|
+
if (missingSections.length === 0 && !shouldPreservePreviousBlocked) {
|
|
951
|
+
return { summary: trimmedSummary, wasRepaired: false, missingSections: [] };
|
|
952
|
+
}
|
|
953
|
+
const repairedSections = [trimmedSummary || "## Goal\n(not captured)"];
|
|
954
|
+
if (!trimmedSummary.includes("## Constraints & Preferences")) {
|
|
955
|
+
repairedSections.push("## Constraints & Preferences\n- (not captured)");
|
|
956
|
+
}
|
|
957
|
+
if (!trimmedSummary.includes("## Progress")) {
|
|
958
|
+
repairedSections.push("## Progress");
|
|
959
|
+
}
|
|
960
|
+
if (!trimmedSummary.includes("### Done")) {
|
|
961
|
+
repairedSections.push("### Done\n- [x] (not captured)");
|
|
962
|
+
}
|
|
963
|
+
if (!trimmedSummary.includes("### In Progress")) {
|
|
964
|
+
repairedSections.push("### In Progress\n- [ ] Continue from preserved context");
|
|
965
|
+
}
|
|
966
|
+
if (!trimmedSummary.includes("### Blocked")) {
|
|
967
|
+
repairedSections.push(`### Blocked\n${previousBlocked && previousBlocked.length > 0 ? previousBlocked : "- (none)"}`);
|
|
968
|
+
}
|
|
969
|
+
else if (shouldPreservePreviousBlocked) {
|
|
970
|
+
repairedSections.push(`### Blocked\n${previousBlocked}`);
|
|
971
|
+
}
|
|
972
|
+
if (!trimmedSummary.includes("## Key Decisions")) {
|
|
973
|
+
repairedSections.push("## Key Decisions\n- **Preserved context**: Summary contract repaired deterministically.");
|
|
974
|
+
}
|
|
975
|
+
if (!trimmedSummary.includes("## Next Steps")) {
|
|
976
|
+
repairedSections.push("## Next Steps\n1. Re-check workspace state before editing.");
|
|
977
|
+
}
|
|
978
|
+
if (!trimmedSummary.includes("## Critical Context")) {
|
|
979
|
+
repairedSections.push(`## Critical Context\n${formatFallbackFacts(options.fallbackFacts)}`);
|
|
980
|
+
}
|
|
981
|
+
if (!trimmedSummary.includes("## Resume Handoff")) {
|
|
982
|
+
repairedSections.push([
|
|
983
|
+
"## Resume Handoff",
|
|
984
|
+
"- **First action**: Re-check workspace state and continue the latest requested task.",
|
|
985
|
+
"- **Re-check before editing**: git status and relevant files from Critical Context.",
|
|
986
|
+
"- **Evidence status**: (not run)",
|
|
987
|
+
].join("\n"));
|
|
988
|
+
}
|
|
989
|
+
return {
|
|
990
|
+
summary: repairedSections.join("\n\n"),
|
|
991
|
+
wasRepaired: true,
|
|
992
|
+
missingSections,
|
|
993
|
+
};
|
|
994
|
+
}
|
|
449
995
|
function createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel) {
|
|
450
996
|
const options = { maxTokens, signal, apiKey, headers };
|
|
451
997
|
if (model.reasoning && thinkingLevel && thinkingLevel !== "off") {
|
|
@@ -464,7 +1010,7 @@ async function completeSummarization(model, context, options, streamFn) {
|
|
|
464
1010
|
* Generate a summary of the conversation using the LLM.
|
|
465
1011
|
* If previousSummary is provided, uses the update prompt to merge.
|
|
466
1012
|
*/
|
|
467
|
-
export async function generateSummary(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn) {
|
|
1013
|
+
export async function generateSummary(currentMessages, model, reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, summaryInputTokens) {
|
|
468
1014
|
const maxTokens = Math.min(Math.floor(0.8 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
|
|
469
1015
|
// Use update prompt if we have a previous summary, otherwise initial prompt
|
|
470
1016
|
let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
|
|
@@ -475,8 +1021,37 @@ export async function generateSummary(currentMessages, model, reserveTokens, api
|
|
|
475
1021
|
// Convert to LLM messages first (handles custom types like bashExecution, custom, etc.)
|
|
476
1022
|
const llmMessages = convertToLlm(currentMessages);
|
|
477
1023
|
const conversationText = serializeConversation(llmMessages);
|
|
1024
|
+
const summaryInputBudgetResolution = resolveSummaryInputTokenBudgetV3(model, maxTokens, {
|
|
1025
|
+
configuredSummaryInputTokens: summaryInputTokens,
|
|
1026
|
+
basePrompt,
|
|
1027
|
+
previousSummary,
|
|
1028
|
+
systemPrompt: SUMMARIZATION_SYSTEM_PROMPT,
|
|
1029
|
+
wrapperText: "<conversation></conversation><previous-summary></previous-summary>",
|
|
1030
|
+
});
|
|
1031
|
+
if (summaryInputBudgetResolution.emergency) {
|
|
1032
|
+
const summary = createEmergencyCompactionHandoff({
|
|
1033
|
+
reason: summaryInputBudgetResolution.reason ?? "summary input budget unavailable",
|
|
1034
|
+
conversationText,
|
|
1035
|
+
previousSummary,
|
|
1036
|
+
});
|
|
1037
|
+
recordHarnessControlEvent("compaction.summary.generated", "completed", {
|
|
1038
|
+
model: model.id,
|
|
1039
|
+
provider: model.provider,
|
|
1040
|
+
maxTokens,
|
|
1041
|
+
emergency: true,
|
|
1042
|
+
reason: summaryInputBudgetResolution.reason,
|
|
1043
|
+
fixedPromptTokens: summaryInputBudgetResolution.fixedPromptTokens,
|
|
1044
|
+
availableInputTokens: summaryInputBudgetResolution.availableInputTokens,
|
|
1045
|
+
});
|
|
1046
|
+
return validateCompactionSummaryContract(summary, {
|
|
1047
|
+
previousSummary,
|
|
1048
|
+
fallbackFacts: extractCriticalFactsFromText(conversationText),
|
|
1049
|
+
}).summary;
|
|
1050
|
+
}
|
|
1051
|
+
const summaryInputBudget = summaryInputBudgetResolution.budgetTokens;
|
|
1052
|
+
const packedConversation = packSummaryInputForTokenBudget(conversationText, summaryInputBudget);
|
|
478
1053
|
// Build the prompt with conversation wrapped in tags
|
|
479
|
-
let promptText = `<conversation>\n${
|
|
1054
|
+
let promptText = `<conversation>\n${packedConversation.text}\n</conversation>\n\n`;
|
|
480
1055
|
if (previousSummary) {
|
|
481
1056
|
promptText += `<previous-summary>\n${previousSummary}\n</previous-summary>\n\n`;
|
|
482
1057
|
}
|
|
@@ -497,7 +1072,22 @@ export async function generateSummary(currentMessages, model, reserveTokens, api
|
|
|
497
1072
|
.filter((c) => c.type === "text")
|
|
498
1073
|
.map((c) => c.text)
|
|
499
1074
|
.join("\n");
|
|
500
|
-
|
|
1075
|
+
const validation = validateCompactionSummaryContract(textContent, {
|
|
1076
|
+
previousSummary,
|
|
1077
|
+
fallbackFacts: extractCriticalFactsFromText(conversationText),
|
|
1078
|
+
});
|
|
1079
|
+
recordHarnessControlEvent("compaction.summary.generated", "completed", {
|
|
1080
|
+
model: model.id,
|
|
1081
|
+
provider: model.provider,
|
|
1082
|
+
maxTokens,
|
|
1083
|
+
summaryInputBudget,
|
|
1084
|
+
packedTokens: packedConversation.packedTokens,
|
|
1085
|
+
omittedTokens: packedConversation.omittedTokens,
|
|
1086
|
+
wasCompressed: packedConversation.wasCompressed,
|
|
1087
|
+
wasRepaired: validation.wasRepaired,
|
|
1088
|
+
missingSections: validation.missingSections,
|
|
1089
|
+
});
|
|
1090
|
+
return validation.summary;
|
|
501
1091
|
}
|
|
502
1092
|
export function prepareCompaction(pathEntries, settings) {
|
|
503
1093
|
if (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === "compaction") {
|
|
@@ -595,16 +1185,16 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
|
|
|
595
1185
|
// Generate both summaries in parallel
|
|
596
1186
|
const [historyResult, turnPrefixResult] = await Promise.all([
|
|
597
1187
|
messagesToSummarize.length > 0
|
|
598
|
-
? generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn)
|
|
1188
|
+
? generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, settings.summaryInputTokens)
|
|
599
1189
|
: Promise.resolve("No prior history."),
|
|
600
|
-
generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, headers, signal, thinkingLevel, streamFn),
|
|
1190
|
+
generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, headers, signal, thinkingLevel, streamFn, settings.summaryInputTokens),
|
|
601
1191
|
]);
|
|
602
1192
|
// Merge into single summary
|
|
603
1193
|
summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
|
|
604
1194
|
}
|
|
605
1195
|
else {
|
|
606
1196
|
// Just generate history summary
|
|
607
|
-
summary = await generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn);
|
|
1197
|
+
summary = await generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, settings.summaryInputTokens);
|
|
608
1198
|
}
|
|
609
1199
|
// Compute file lists and append to summary
|
|
610
1200
|
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
|
@@ -622,11 +1212,17 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
|
|
|
622
1212
|
/**
|
|
623
1213
|
* Generate a summary for a turn prefix (when splitting a turn).
|
|
624
1214
|
*/
|
|
625
|
-
async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey, headers, signal, thinkingLevel, streamFn) {
|
|
1215
|
+
async function generateTurnPrefixSummary(messages, model, reserveTokens, apiKey, headers, signal, thinkingLevel, streamFn, summaryInputTokens) {
|
|
626
1216
|
const maxTokens = Math.min(Math.floor(0.5 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY); // Smaller budget for turn prefix
|
|
627
1217
|
const llmMessages = convertToLlm(messages);
|
|
628
1218
|
const conversationText = serializeConversation(llmMessages);
|
|
629
|
-
const
|
|
1219
|
+
const packedConversation = packSummaryInputForTokenBudget(conversationText, resolveSummaryInputTokenBudget(model, maxTokens, {
|
|
1220
|
+
configuredSummaryInputTokens: summaryInputTokens,
|
|
1221
|
+
basePrompt: TURN_PREFIX_SUMMARIZATION_PROMPT,
|
|
1222
|
+
systemPrompt: SUMMARIZATION_SYSTEM_PROMPT,
|
|
1223
|
+
wrapperText: "<conversation></conversation>",
|
|
1224
|
+
}));
|
|
1225
|
+
const promptText = `<conversation>\n${packedConversation.text}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
|
630
1226
|
const summarizationMessages = [
|
|
631
1227
|
{
|
|
632
1228
|
role: "user",
|