dsh-context-pruner 0.0.0-stage → 0.1.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.
@@ -0,0 +1,19 @@
1
+ # dsh-context-pruner bundle patch.
2
+ #
3
+ # Mounts the plugin into a dsh profile's plugin tree. This build registers the
4
+ # read-only `/prune` command; it does NOT replace `ctx.compaction`, so it
5
+ # composes safely alongside the shipped `compaction-basic` row.
6
+ #
7
+ # Install:
8
+ # dsh plugin --profile web add G:\Projects\dsh-context-pruner
9
+ #
10
+ # The plugin command pnpm-installs the package into the profile's node_modules
11
+ # and reconciles it into the profile's `dsh.profile.bundles` list, after which
12
+ # this patch's insert row is applied. Nothing outside the profile directory is
13
+ # modified, and removing the row (or the bundle entry) fully reverts it.
14
+ - insert:
15
+ - id: context-pruner
16
+ # Node-resolvable package name — must stay in sync with package.json
17
+ # `name`. Quoted because it is a plain scalar that may be read as a
18
+ # reserved token.
19
+ name: 'dsh-context-pruner'
package/lib/backend.js ADDED
@@ -0,0 +1,190 @@
1
+ /**
2
+ * dsh-context-pruner corrective compaction backend.
3
+ *
4
+ * The whole point of this plugin lives in one method: `summarize()`. DSH's
5
+ * `BasicCompactionEngine` documents it as the *sole* subclass customization
6
+ * hook — pressure policy, retention, range selection, shrink validation and
7
+ * token accounting all stay on the parent, so every pricing decision keeps
8
+ * using the singleton token meter.
9
+ *
10
+ * We do NOT reimplement the region transaction and we do NOT select a range.
11
+ * We only change *what the summary says*: instead of a neutral "here is what
12
+ * happened" checkpoint, we emit a **correction checkpoint** that explicitly
13
+ * retires the failed exploration and states the conclusion that actually
14
+ * holds.
15
+ *
16
+ * ## Why this reimplements the call instead of swapping the instruction
17
+ *
18
+ * `dsh-compaction-basic` builds its summarization directive from a
19
+ * module-scoped `COMPACTION_INSTRUCTION` constant that it does not export, and
20
+ * `summarizeWithLlm()` is a plain module function. There is no seam to
21
+ * substitute the instruction, so the honest options are (a) monkey-patch
22
+ * module scope or (b) reimplement the call here. We reimplement it, mirroring
23
+ * the shipped shape exactly so the provider's warm prefix cache is still
24
+ * reused: the conversation's own system prompt, tools, and leading messages go
25
+ * first, and our instruction is the single novel trailing user message.
26
+ *
27
+ * @module dsh-context-pruner/backend
28
+ */
29
+
30
+ import { BasicCompactionEngine } from "@deepseek-ai/dsh-compaction-basic";
31
+ import { BlockAssembler, LlmError, contentHasImage } from "@deepseek-ai/dsh-llm";
32
+ import { deepFreeze } from "@deepseek-ai/dsh-util-values";
33
+
34
+ /**
35
+ * Default instruction appended as the final user message of the auxiliary
36
+ * summarization call.
37
+ *
38
+ * It deliberately contradicts the stock compaction instruction. The stock one
39
+ * says "Do NOT mention this summarization request or that the context was
40
+ * compacted"; ours must say the opposite, because the entire value of the
41
+ * feature is that the model *stops* treating the retired exploration as live
42
+ * reasoning.
43
+ */
44
+ export const DEFAULT_CORRECTIVE_INSTRUCTION = [
45
+ "You are now acting as a CORRECTION CHECKPOINT writer for this AI coding assistant.",
46
+ "",
47
+ "The conversation ABOVE is being replaced by your checkpoint. It contains an exploration that reached a WRONG conclusion.",
48
+ "Your checkpoint must let another model resume the work WITHOUT re-trusting that wrong conclusion.",
49
+ "",
50
+ "Output EXACTLY the Markdown structure below: keep every section, in order. Use terse bullets, not prose paragraphs. Write \"(none)\" for an empty section — never drop a section.",
51
+ "",
52
+ "## Original Request and Intent",
53
+ "- [the user's original and evolving goals; quote verbatim where the exact wording matters]",
54
+ "",
55
+ "## Retired Exploration — DO NOT TRUST",
56
+ "- [one or two bullets naming what was attempted above and why it was wrong or abandoned]",
57
+ "",
58
+ "## Authoritative Conclusion",
59
+ "- [what is actually true now, or the correct next step]",
60
+ "",
61
+ "## Carried-Forward Facts",
62
+ "- [only facts from above that are STILL TRUE and still needed: file paths, identifiers, hard constraints, user preferences, explicit user instructions]",
63
+ "",
64
+ "## Next Step",
65
+ "- [the single next action in line with the most recent request, or \"(none)\"]",
66
+ "",
67
+ "Rules:",
68
+ "- Name the wrong conclusion ONLY as a rejected direction. Never restate it as a finding, and never re-derive it.",
69
+ "- Do NOT reproduce long transcripts, code dumps, or command output from the replaced span.",
70
+ "- Preserve exact file paths, commands, error strings, identifiers, numeric values, function signatures, and syntax fragments verbatim.",
71
+ "- Capture user feedback and explicit instructions faithfully, especially corrections.",
72
+ "- Be brief: this checkpoint must be much shorter than the span it replaces.",
73
+ "- Output only the checkpoint text: do not call any tool and do not take any other action.",
74
+ "- If the conversation already contains a <compacted-summary> block, it is a PRIOR checkpoint. Do not copy it forward verbatim: preserve still-true facts, drop stale ones, and merge everything into ONE consolidated checkpoint under the same structure."
75
+ ].join("\n");
76
+
77
+ /** Reject visual output and keep only text, mirroring the shipped projection. */
78
+ function summaryText(blocks) {
79
+ if (contentHasImage(blocks)) {
80
+ throw new LlmError("compaction summary cannot contain image output", "UNSUPPORTED_CONTENT");
81
+ }
82
+ return blocks.filter((block) => block.type === "text");
83
+ }
84
+
85
+ /**
86
+ * Map a terminal finish reason to the failure the shipped backend raises,
87
+ * mirroring `finishError` in `dsh-compaction-basic`.
88
+ */
89
+ function finishError(finish) {
90
+ switch (finish.kind) {
91
+ case "error":
92
+ case "aborted":
93
+ return new LlmError(finish.failure.message, finish.failure.code, finish.failure);
94
+ case "max-tokens": {
95
+ const error = new Error("summarization truncated at the token cap (incomplete checkpoint)");
96
+ error.code = "MAX_TOKENS";
97
+ return error;
98
+ }
99
+ default:
100
+ return undefined;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Compaction backend that reuses the shipped engine's transaction, policy and
106
+ * metering, changing only the summarization directive.
107
+ *
108
+ * Inherited behavior is deliberate:
109
+ * - `compactNow()` — the idle-safe manual path behind `/compact` and `/prune`.
110
+ * - `compactRegion()` — needs an open turn; never used by this plugin.
111
+ * - automatic pressure / overflow recovery, token metering, shrink validation.
112
+ */
113
+ export class CorrectiveCompactionEngine extends BasicCompactionEngine {
114
+ /**
115
+ * Override the sole summarization hook to emit a correction checkpoint.
116
+ *
117
+ * Mirrors the shipped implementation's call shape so the provider's warm
118
+ * prefix cache is still reused, but substitutes the corrective directive.
119
+ *
120
+ * @param input - replayed conversation prefix (system, tools, leading messages) to condense.
121
+ * @param agent - supplies routed-model history and fallback model.
122
+ * @param signal - optional cancellation forwarded to the adapter.
123
+ * @returns safe text summary blocks plus the exact call envelope.
124
+ */
125
+ async summarize(input, agent, signal) {
126
+ const config = this.config;
127
+ const latest = agent.session.requestHeader()?.config;
128
+ const configured =
129
+ config.summarizationProvider.length === 0
130
+ ? undefined
131
+ : { provider: config.summarizationProvider, model: config.summarizationModel };
132
+ const agentTarget =
133
+ agent.options.provider !== undefined &&
134
+ agent.options.provider.length > 0 &&
135
+ agent.options.model !== undefined &&
136
+ agent.options.model.length > 0
137
+ ? { provider: agent.options.provider, model: agent.options.model }
138
+ : undefined;
139
+ const target = configured ?? latest ?? agentTarget;
140
+ if (target === undefined) {
141
+ throw new Error(
142
+ "no provider/model available for summarization: set both summarization fields, route one request, or set both AgentOptions fields"
143
+ );
144
+ }
145
+
146
+ const instruction =
147
+ typeof this.correctiveInstruction === "string" && this.correctiveInstruction.length > 0
148
+ ? this.correctiveInstruction
149
+ : DEFAULT_CORRECTIVE_INSTRUCTION;
150
+
151
+ const assembler = new BlockAssembler();
152
+ const messages = [
153
+ ...input.messages,
154
+ deepFreeze({ role: "user", content: [{ type: "text", text: instruction }] })
155
+ ];
156
+ const options = {
157
+ provider: target.provider,
158
+ model: target.model,
159
+ messages,
160
+ toolHistory: agent.session.toolHistory(),
161
+ ...(input.tools === undefined ? {} : { tools: [...input.tools] }),
162
+ maxTokens: config.maxTokens,
163
+ sessionId: agent.session.id,
164
+ purpose: "compaction",
165
+ ...(signal === undefined ? {} : { signal })
166
+ };
167
+
168
+ for await (const chunk of this.ctx.llm.stream(options)) assembler.push(chunk);
169
+ const error = finishError(assembler.finish);
170
+ if (error !== undefined) throw error;
171
+
172
+ const rawOutput = assembler.blocks();
173
+ const summary = summaryText(rawOutput);
174
+ if (!summary.some((block) => block.text.trim().length > 0)) {
175
+ throw new Error("summarization produced no text summary content");
176
+ }
177
+
178
+ return {
179
+ summary,
180
+ rawOutput,
181
+ llmStreamCall: true,
182
+ provider: options.provider,
183
+ model: options.model,
184
+ maxTokens: config.maxTokens,
185
+ ...(assembler.usage === undefined ? {} : { usage: assembler.usage })
186
+ };
187
+ }
188
+ }
189
+
190
+ export default CorrectiveCompactionEngine;