generateSummary function

Future<String> generateSummary(
  1. List<Message> messages, {
  2. required SummarizeFn summarize,
  3. String? customInstructions,
  4. String? previousSummary,
  5. CancelToken? cancelToken,
  6. CompactionPrompts prompts = defaultCompactionPrompts,
  7. String? userRequestCandidates,
  8. int? maxPromptTokens,
  9. PinnedOperativePayload? pinnedOperative,
})

Generate (or update) a conversation summary for compaction.

Ported from pi's generateSummary: serializes messages into <conversation> tags, optionally appends <previous-checkpoint> for iterative updates, and ends with the fixed structured prompt (plus Additional focus: when customInstructions is given). Throws CompactionException on failure — callers must treat compaction as failure-safe and leave history untouched.

maxPromptTokens (issue #729) bounds the outbound payload: when the built prompt estimates over it, the region is summarized CHUNK-WISE — each chunk under maxPromptTokens, every chunk's summary threaded into the next as <previous-checkpoint> (the existing iterative-update mechanism; the first chunk uses the plain summary prompt, later chunks the update prompt) — and the last fold is returned. A single message bigger than the budget is truncated with an explicit note. Under the budget (and when null) the prompt is byte-identical to the unbounded path.

Implementation

Future<String> generateSummary(
  List<Message> messages, {
  required SummarizeFn summarize,
  String? customInstructions,
  String? previousSummary,
  CancelToken? cancelToken,
  CompactionPrompts prompts = defaultCompactionPrompts,
  String? userRequestCandidates,
  int? maxPromptTokens,
  PinnedOperativePayload? pinnedOperative,
}) async {
  // Issue #1131: the previous checkpoint re-enters this prompt verbatim —
  // heal it first so a poisoned old summary cannot be paraphrased forward
  // into a fresh one (idempotent; clean records are unaffected). gh-1409:
  // pinned lines ride the heal protected (AC4) — a pin inside an old
  // checkpoint survives sanitization byte-identical.
  previousSummary = previousSummary == null
      ? null
      : sanitizeSummary(
          previousSummary,
          protectedLines: pinnedOperative?.lines ?? const {},
        ).text;
  var basePrompt = previousSummary != null
      ? prompts.summaryUpdate
      : prompts.summary;
  // gh-1409 AC3: the verbatim-preserve duty rides the instruction tail of
  // every call whose input carries the pinned block.
  if (pinnedOperative != null) {
    basePrompt = '$basePrompt\n\n${prompts.pinnedOperative}';
  }
  if (customInstructions != null) {
    basePrompt = '$basePrompt\n\nAdditional focus: $customInstructions';
  }
  String build(
    String conversation,
    String? candidates,
    String? prior,
    String instructions,
  ) {
    final prompt = StringBuffer()
      ..write('<conversation>\n')
      ..write(conversation)
      ..write('\n</conversation>\n\n');
    if (candidates != null) {
      prompt
        ..write(candidates)
        ..write('\n\n');
    }
    if (pinnedOperative != null) {
      prompt
        ..write(pinnedOperative.block)
        ..write('\n\n');
    }
    if (prior != null) {
      prompt.write('<previous-checkpoint>\n$prior\n</previous-checkpoint>\n\n');
    }
    prompt.write(instructions);
    return prompt.toString();
  }

  final full = build(
    serializeConversation(messages),
    userRequestCandidates ?? userRequestCandidatesBlock(messages),
    previousSummary,
    basePrompt,
  );
  if (maxPromptTokens == null ||
      estimateStringTokens(full) <= maxPromptTokens) {
    return _runSummarization(
      prompt: full,
      summarize: summarize,
      cancelToken: cancelToken,
      failureLabel: 'Summarization failed',
    );
  }

  // Over the payload budget (#729): chunk the region, fold chunk-wise.
  // Each chunk carries only its own request candidates (the whole-range
  // block would ride every chunk and re-inflate the payload); the record
  // `<id>` pointers degrade to date-only on this path.
  var prior = previousSummary;
  var fold = '';
  final chunks = chunkSummarizableMessages(
    messages,
    max(maxPromptTokens - _chunkEnvelopeReserveTokens, 256),
  );
  for (var i = 0; i < chunks.length; i++) {
    final candidates = userRequestCandidatesBlock(chunks[i]);
    final instructions = i == 0
        ? basePrompt
        : (pinnedOperative == null
              ? prompts.summaryUpdate
              // E7: the verbatim-preserve duty rides every chunk.
              : '${prompts.summaryUpdate}\n\n${prompts.pinnedOperative}');
    final conversation = truncateForSummaryBudget(
      serializeConversation(chunks[i]),
      budgetTokens: maxPromptTokens,
      envelopeChars: _summaryEnvelopeChars(
        candidates: candidates,
        prior: prior,
        instructions: instructions,
        pinnedBlock: pinnedOperative?.block,
      ),
    );
    fold = await _runSummarization(
      prompt: build(conversation, candidates, prior, instructions),
      summarize: summarize,
      cancelToken: cancelToken,
      failureLabel: 'Summarization failed',
    );
    prior = fold;
  }
  return fold;
}