@archships/dim-plugin-auto-compact
Advanced tools
+55
-28
@@ -14,2 +14,3 @@ //#region src/index.ts | ||
| const CONTINUATION_SUMMARY_BLOCK_PATTERN = /<continuation_summary>([\s\S]*?)<\/continuation_summary>/g; | ||
| const CONTINUATION_SUMMARY_TAG_FRAGMENT_PATTERN = /<\/?continuation_summary>?/g; | ||
| const SUMMARY_PROMPT_VERSION = "continuation-summary-v2"; | ||
@@ -133,2 +134,3 @@ const COMPACTION_INJECTION_METADATA_KEY = "_dimCompactionInjection"; | ||
| summaryPrompt: normalized.summaryPrompt, | ||
| existingSummary: payload.systemSegments[0], | ||
| systemSegments: payload.systemSegments, | ||
@@ -142,2 +144,18 @@ compactedMessages: plan.compactedMessages, | ||
| }); | ||
| if (summaryResult.status === "empty_body_preserve_previous") { | ||
| await persistPluginState(context.sessionId, context, { | ||
| strategy: "handoff-v1", | ||
| summaryModel, | ||
| entries: previous.entries, | ||
| lastAttempt: finalizeAttemptDiagnostics(baseAttempt, { | ||
| status: "skipped", | ||
| reasonCode: "empty_summary_preserved_previous", | ||
| reasonMessage: "Auto compact summary normalized to an empty body and preserved the previous continuation summary", | ||
| compactedMessageCount: plan.compactedMessages.length, | ||
| retainedMessageCount: plan.retainedMessages.length | ||
| }) | ||
| }); | ||
| return; | ||
| } | ||
| if (summaryResult.status === "invalid_summary_contract") throw createCodedError("invalid_summary_contract", "Auto compact summary body cannot be empty"); | ||
| const plannedAttempt = finalizeAttemptDiagnostics(baseAttempt, { | ||
@@ -332,17 +350,24 @@ status: "planned", | ||
| const summaryInputBudget = Math.max(256, input.summaryInputMaxTokens - DEFAULT_SUMMARY_INPUT_SAFETY_MARGIN); | ||
| const summarized = await summarizeRenderedSections({ | ||
| stage: "source", | ||
| remainingLevels: MAX_SUMMARY_RECURSION_PASSES, | ||
| prefixSections: renderedInput.prefixSections, | ||
| messageSections: renderedInput.messageSections, | ||
| summaryInputBudget, | ||
| sessionId: input.sessionId, | ||
| summaryModel: input.summaryModel, | ||
| maxSummaryTokens: input.maxSummaryTokens, | ||
| summaryPrompt: input.summaryPrompt, | ||
| usageKind: input.usageKind, | ||
| model: input.model, | ||
| logger: input.logger | ||
| }); | ||
| let summarized; | ||
| try { | ||
| summarized = await summarizeRenderedSections({ | ||
| stage: "source", | ||
| remainingLevels: MAX_SUMMARY_RECURSION_PASSES, | ||
| prefixSections: renderedInput.prefixSections, | ||
| messageSections: renderedInput.messageSections, | ||
| summaryInputBudget, | ||
| sessionId: input.sessionId, | ||
| summaryModel: input.summaryModel, | ||
| maxSummaryTokens: input.maxSummaryTokens, | ||
| summaryPrompt: input.summaryPrompt, | ||
| usageKind: input.usageKind, | ||
| model: input.model, | ||
| logger: input.logger | ||
| }); | ||
| } catch (error) { | ||
| if (readErrorCode(error) === "empty_summary_body") return input.existingSummary?.trim() ? { status: "empty_body_preserve_previous" } : { status: "invalid_summary_contract" }; | ||
| throw error; | ||
| } | ||
| return { | ||
| status: "normalized", | ||
| summary: summarized.summary, | ||
@@ -463,4 +488,5 @@ summaryNormalization: summarized.summaryNormalization, | ||
| } | ||
| if (!text.trim()) throw createCodedError("invalid_summary_contract", "Auto compact summary body cannot be empty"); | ||
| if (!text.trim()) throw createCodedError("empty_summary_body", "Auto compact summary body cannot be empty"); | ||
| const normalizedSummary = normalizeContinuationSummary(text); | ||
| if (normalizedSummary.status === "empty_body") throw createCodedError("empty_summary_body", "Auto compact summary body cannot be empty"); | ||
| if (normalizedSummary.normalization !== "plain_text") input.logger.emit({ | ||
@@ -657,21 +683,19 @@ level: "warn", | ||
| const trimmed = summary.trim(); | ||
| const openCount = trimmed.split(CONTINUATION_SUMMARY_OPEN_TAG).length - 1; | ||
| const closeCount = trimmed.split(CONTINUATION_SUMMARY_CLOSE_TAG).length - 1; | ||
| if (!trimmed) return { status: "empty_body" }; | ||
| const blockMatches = [...trimmed.matchAll(CONTINUATION_SUMMARY_BLOCK_PATTERN)]; | ||
| if (blockMatches.length > 1) throw createCodedError("invalid_summary_contract", "Auto compact summary must be plain text or contain exactly one <continuation_summary>...</continuation_summary> block"); | ||
| if (blockMatches.length === 1) { | ||
| if (openCount !== 1 || closeCount !== 1) throw createCodedError("invalid_summary_contract", "Auto compact summary must be plain text or contain exactly one <continuation_summary>...</continuation_summary> block"); | ||
| const blockBodies = blockMatches.map((match) => match?.[1]?.trim() ?? "").filter((body) => body.length > 0); | ||
| if (blockBodies.length > 0) { | ||
| const [match] = blockMatches; | ||
| const body = match?.[1]?.trim(); | ||
| if (!body) throw createCodedError("invalid_summary_contract", "Auto compact summary body cannot be empty"); | ||
| return { | ||
| summary: renderCanonicalContinuationSummary(body), | ||
| normalization: match.index === 0 && match[0].length === trimmed.length ? "wrapped_block" : "extracted_block" | ||
| status: "normalized", | ||
| summary: renderCanonicalContinuationSummary(blockBodies.join("\n\n")), | ||
| normalization: blockMatches.length === 1 && match?.index === 0 && match[0].length === trimmed.length ? "wrapped_block" : "extracted_block" | ||
| }; | ||
| } | ||
| if (openCount > 0 || closeCount > 0) throw createCodedError("invalid_summary_contract", "Auto compact summary must be plain text or contain exactly one <continuation_summary>...</continuation_summary> block"); | ||
| if (!trimmed) throw createCodedError("invalid_summary_contract", "Auto compact summary body cannot be empty"); | ||
| const stripped = stripContinuationSummaryTagFragments(trimmed).trim(); | ||
| if (!stripped) return { status: "empty_body" }; | ||
| return { | ||
| summary: renderCanonicalContinuationSummary(trimmed), | ||
| normalization: "plain_text" | ||
| status: "normalized", | ||
| summary: renderCanonicalContinuationSummary(stripped), | ||
| normalization: stripped === trimmed ? "plain_text" : "extracted_block" | ||
| }; | ||
@@ -682,2 +706,5 @@ } | ||
| } | ||
| function stripContinuationSummaryTagFragments(text) { | ||
| return text.replace(CONTINUATION_SUMMARY_TAG_FRAGMENT_PATTERN, "\n"); | ||
| } | ||
| function combineSummaryNormalizationModes(left, right) { | ||
@@ -684,0 +711,0 @@ const rank = { |
+1
-1
| { | ||
| "name": "@archships/dim-plugin-auto-compact", | ||
| "version": "0.0.12", | ||
| "version": "0.0.13", | ||
| "description": "Official auto compaction plugin for dim-agent-sdk.", | ||
@@ -5,0 +5,0 @@ "homepage": "https://dimcode.dev/", |
+3
-3
@@ -55,5 +55,5 @@ # @archships/dim-plugin-auto-compact | ||
| - summary model calls now opt into the SDK usage ledger with semantic request kinds: threshold-triggered passes record `auto_compaction`, and `session.compact()` summary calls record `manual_compaction` | ||
| - the default summary prompt asks the model for plain-text summary body only; runtime wraps accepted output back into one canonical `<continuation_summary>...</continuation_summary>` block, and it continues to accept a single wrapped block with or without surrounding noise for backward compatibility | ||
| - multiple wrapper blocks, empty bodies, or malformed wrapper tags still fail with `invalid_summary_contract` | ||
| - `lastAttempt` is updated on every threshold-triggered compaction pass with `planned` / `skipped` / `compacted` / `failed`, budget metadata, estimator-based `estimatedInputTokensAfter` / `estimatedSavedTokens`, reason codes such as `missing_summary_model`, `nothing_to_compact`, `summary_generation_failed`, or `compaction_owner_required`, and optional `summaryNormalization` values (`plain_text`, `wrapped_block`, `extracted_block`) when the summary output was inspected | ||
| - the default summary prompt asks the model for plain-text summary body only; runtime normalizes accepted output back into one canonical `<continuation_summary>...</continuation_summary>` block before replay, including wrapped blocks, merged multi-block outputs, and malformed-tag fragments that still yield a non-empty semantic body | ||
| - when normalization yields an empty body, runtime preserves the previous saved continuation summary when one already exists; only empty output without a previous summary fails with `invalid_summary_contract` | ||
| - `lastAttempt` is updated on every threshold-triggered compaction pass with `planned` / `skipped` / `compacted` / `failed`, budget metadata, estimator-based `estimatedInputTokensAfter` / `estimatedSavedTokens`, reason codes such as `missing_summary_model`, `nothing_to_compact`, `empty_summary_preserved_previous`, `summary_generation_failed`, or `compaction_owner_required`, and optional `summaryNormalization` values (`plain_text`, `wrapped_block`, `extracted_block`) when the summary output was inspected | ||
| - persisted plugin state keeps only semantic provenance fields; summary-call counters and per-message id arrays stay out of `pluginState` | ||
@@ -60,0 +60,0 @@ - `context.compacted` notifications now include compacted / retained message counts plus estimator-based before / after / saved token fields when budgeting is enabled |
Sorry, the diff of this file is too big to display
122622
2.68%867
3.21%