@royalcat/opencode-dcp-rc 0.0.0-stage → 4.0.0
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/LICENSE +619 -0
- package/README.md +348 -2
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7611 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/commands/compression-targets.d.ts +13 -0
- package/dist/lib/commands/compression-targets.d.ts.map +1 -0
- package/dist/lib/commands/context.d.ts +67 -0
- package/dist/lib/commands/context.d.ts.map +1 -0
- package/dist/lib/commands/decompress.d.ts +12 -0
- package/dist/lib/commands/decompress.d.ts.map +1 -0
- package/dist/lib/commands/help.d.ts +18 -0
- package/dist/lib/commands/help.d.ts.map +1 -0
- package/dist/lib/commands/index.d.ts +8 -0
- package/dist/lib/commands/index.d.ts.map +1 -0
- package/dist/lib/commands/manual.d.ts +23 -0
- package/dist/lib/commands/manual.d.ts.map +1 -0
- package/dist/lib/commands/recompress.d.ts +12 -0
- package/dist/lib/commands/recompress.d.ts.map +1 -0
- package/dist/lib/commands/stats.d.ts +25 -0
- package/dist/lib/commands/stats.d.ts.map +1 -0
- package/dist/lib/commands/sweep.d.ts +23 -0
- package/dist/lib/commands/sweep.d.ts.map +1 -0
- package/dist/lib/compress/index.d.ts +3 -0
- package/dist/lib/compress/index.d.ts.map +1 -0
- package/dist/lib/compress/pipeline.d.ts +29 -0
- package/dist/lib/compress/pipeline.d.ts.map +1 -0
- package/dist/lib/compress/protected-content.d.ts +6 -0
- package/dist/lib/compress/protected-content.d.ts.map +1 -0
- package/dist/lib/compress/rc.d.ts +15 -0
- package/dist/lib/compress/rc.d.ts.map +1 -0
- package/dist/lib/compress/search.d.ts +12 -0
- package/dist/lib/compress/search.d.ts.map +1 -0
- package/dist/lib/compress/state.d.ts +10 -0
- package/dist/lib/compress/state.d.ts.map +1 -0
- package/dist/lib/compress/summary.d.ts +58 -0
- package/dist/lib/compress/summary.d.ts.map +1 -0
- package/dist/lib/compress/timing.d.ts +18 -0
- package/dist/lib/compress/timing.d.ts.map +1 -0
- package/dist/lib/compress/types.d.ts +55 -0
- package/dist/lib/compress/types.d.ts.map +1 -0
- package/dist/lib/compress-permission.d.ts +4 -0
- package/dist/lib/compress-permission.d.ts.map +1 -0
- package/dist/lib/config.d.ts +83 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/hooks.d.ts +22 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/lib/logger.d.ts +31 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/message-ids.d.ts +20 -0
- package/dist/lib/message-ids.d.ts.map +1 -0
- package/dist/lib/messages/index.d.ts +8 -0
- package/dist/lib/messages/index.d.ts.map +1 -0
- package/dist/lib/messages/inject/inject.d.ts +7 -0
- package/dist/lib/messages/inject/inject.d.ts.map +1 -0
- package/dist/lib/messages/inject/subagent-results.d.ts +4 -0
- package/dist/lib/messages/inject/subagent-results.d.ts.map +1 -0
- package/dist/lib/messages/inject/utils.d.ts +23 -0
- package/dist/lib/messages/inject/utils.d.ts.map +1 -0
- package/dist/lib/messages/prune.d.ts +4 -0
- package/dist/lib/messages/prune.d.ts.map +1 -0
- package/dist/lib/messages/query.d.ts +7 -0
- package/dist/lib/messages/query.d.ts.map +1 -0
- package/dist/lib/messages/reasoning-strip.d.ts +8 -0
- package/dist/lib/messages/reasoning-strip.d.ts.map +1 -0
- package/dist/lib/messages/shape.d.ts +5 -0
- package/dist/lib/messages/shape.d.ts.map +1 -0
- package/dist/lib/messages/sync.d.ts +4 -0
- package/dist/lib/messages/sync.d.ts.map +1 -0
- package/dist/lib/messages/utils.d.ts +27 -0
- package/dist/lib/messages/utils.d.ts.map +1 -0
- package/dist/lib/prompts/compress-rc.d.ts +4 -0
- package/dist/lib/prompts/compress-rc.d.ts.map +1 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts +2 -0
- package/dist/lib/prompts/context-limit-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/nudge.d.ts +4 -0
- package/dist/lib/prompts/extensions/nudge.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/system.d.ts +4 -0
- package/dist/lib/prompts/extensions/system.d.ts.map +1 -0
- package/dist/lib/prompts/extensions/tool.d.ts +3 -0
- package/dist/lib/prompts/extensions/tool.d.ts.map +1 -0
- package/dist/lib/prompts/index.d.ts +4 -0
- package/dist/lib/prompts/index.d.ts.map +1 -0
- package/dist/lib/prompts/iteration-nudge.d.ts +2 -0
- package/dist/lib/prompts/iteration-nudge.d.ts.map +1 -0
- package/dist/lib/prompts/store.d.ts +26 -0
- package/dist/lib/prompts/store.d.ts.map +1 -0
- package/dist/lib/prompts/system.d.ts +4 -0
- package/dist/lib/prompts/system.d.ts.map +1 -0
- package/dist/lib/prompts/turn-nudge.d.ts +2 -0
- package/dist/lib/prompts/turn-nudge.d.ts.map +1 -0
- package/dist/lib/protected-patterns.d.ts +6 -0
- package/dist/lib/protected-patterns.d.ts.map +1 -0
- package/dist/lib/state/index.d.ts +5 -0
- package/dist/lib/state/index.d.ts.map +1 -0
- package/dist/lib/state/persistence.d.ts +45 -0
- package/dist/lib/state/persistence.d.ts.map +1 -0
- package/dist/lib/state/state.d.ts +9 -0
- package/dist/lib/state/state.d.ts.map +1 -0
- package/dist/lib/state/tool-cache.d.ts +13 -0
- package/dist/lib/state/tool-cache.d.ts.map +1 -0
- package/dist/lib/state/types.d.ts +102 -0
- package/dist/lib/state/types.d.ts.map +1 -0
- package/dist/lib/state/utils.d.ts +22 -0
- package/dist/lib/state/utils.d.ts.map +1 -0
- package/dist/lib/strategies/deduplication.d.ts +10 -0
- package/dist/lib/strategies/deduplication.d.ts.map +1 -0
- package/dist/lib/strategies/index.d.ts +3 -0
- package/dist/lib/strategies/index.d.ts.map +1 -0
- package/dist/lib/strategies/purge-errors.d.ts +13 -0
- package/dist/lib/strategies/purge-errors.d.ts.map +1 -0
- package/dist/lib/subagents/subagent-results.d.ts +5 -0
- package/dist/lib/subagents/subagent-results.d.ts.map +1 -0
- package/dist/lib/token-utils.d.ts +19 -0
- package/dist/lib/token-utils.d.ts.map +1 -0
- package/dist/lib/tui/dialogs.d.ts +29 -0
- package/dist/lib/tui/dialogs.d.ts.map +1 -0
- package/dist/lib/tui/format.d.ts +4 -0
- package/dist/lib/tui/format.d.ts.map +1 -0
- package/dist/lib/tui/types.d.ts +18 -0
- package/dist/lib/tui/types.d.ts.map +1 -0
- package/dist/lib/tui/ui.d.ts +48 -0
- package/dist/lib/tui/ui.d.ts.map +1 -0
- package/dist/lib/ui/notification.d.ts +17 -0
- package/dist/lib/ui/notification.d.ts.map +1 -0
- package/dist/lib/ui/utils.d.ts +10 -0
- package/dist/lib/ui/utils.d.ts.map +1 -0
- package/dist/lib/v2/index.d.ts +5 -0
- package/dist/lib/v2/index.d.ts.map +1 -0
- package/dist/lib/v2/messages.d.ts +13 -0
- package/dist/lib/v2/messages.d.ts.map +1 -0
- package/dist/lib/v2/rpc.d.ts +55 -0
- package/dist/lib/v2/rpc.d.ts.map +1 -0
- package/dist/lib/v2/theme.d.ts +55 -0
- package/dist/lib/v2/theme.d.ts.map +1 -0
- package/dist/lib/v2/tui.d.ts +4 -0
- package/dist/lib/v2/tui.d.ts.map +1 -0
- package/dist/tui.d.ts +7 -0
- package/dist/tui.d.ts.map +1 -0
- package/lib/commands/compression-targets.ts +54 -0
- package/lib/commands/context.ts +305 -0
- package/lib/commands/decompress.ts +273 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +126 -0
- package/lib/commands/recompress.ts +222 -0
- package/lib/commands/stats.ts +159 -0
- package/lib/commands/sweep.ts +266 -0
- package/lib/compress/index.ts +2 -0
- package/lib/compress/pipeline.ts +116 -0
- package/lib/compress/protected-content.ts +163 -0
- package/lib/compress/rc.ts +458 -0
- package/lib/compress/search.ts +277 -0
- package/lib/compress/state.ts +272 -0
- package/lib/compress/summary.ts +293 -0
- package/lib/compress/timing.ts +77 -0
- package/lib/compress/types.ts +57 -0
- package/lib/compress-permission.ts +9 -0
- package/lib/config.ts +995 -0
- package/lib/hooks.ts +205 -0
- package/lib/logger.ts +226 -0
- package/lib/message-ids.ts +181 -0
- package/lib/messages/index.ts +7 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +84 -0
- package/lib/messages/inject/utils.ts +304 -0
- package/lib/messages/prune.ts +238 -0
- package/lib/messages/query.ts +71 -0
- package/lib/messages/reasoning-strip.ts +40 -0
- package/lib/messages/shape.ts +50 -0
- package/lib/messages/sync.ts +124 -0
- package/lib/messages/utils.ts +190 -0
- package/lib/prompts/compress-rc.ts +25 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +34 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +21 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +452 -0
- package/lib/prompts/system.ts +41 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +159 -0
- package/lib/state/index.ts +4 -0
- package/lib/state/persistence.ts +305 -0
- package/lib/state/state.ts +210 -0
- package/lib/state/tool-cache.ts +99 -0
- package/lib/state/types.ts +114 -0
- package/lib/state/utils.ts +345 -0
- package/lib/strategies/deduplication.ts +125 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +86 -0
- package/lib/subagents/subagent-results.ts +75 -0
- package/lib/token-utils.ts +164 -0
- package/lib/tui/dialogs.tsx +237 -0
- package/lib/tui/format.ts +25 -0
- package/lib/tui/types.ts +25 -0
- package/lib/tui/ui.tsx +245 -0
- package/lib/ui/notification.ts +347 -0
- package/lib/ui/utils.ts +304 -0
- package/lib/v2/index.ts +401 -0
- package/lib/v2/messages.ts +252 -0
- package/lib/v2/rpc.ts +48 -0
- package/lib/v2/theme.ts +67 -0
- package/lib/v2/tui.tsx +115 -0
- package/package.json +91 -5
- package/server.js +1 -0
- package/tui.tsx +7 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import type { ToolContext, SelectionResolution } from "./types"
|
|
3
|
+
import type { WithParts } from "../state"
|
|
4
|
+
import { refreshManualMode } from "../state"
|
|
5
|
+
import { formatBlockRef, formatMessageRef, parseBoundaryId, type IdFormat } from "../message-ids"
|
|
6
|
+
import {
|
|
7
|
+
buildSearchContext,
|
|
8
|
+
fetchSessionMessages,
|
|
9
|
+
resolveAnchorMessageId,
|
|
10
|
+
resolveBoundaryIds,
|
|
11
|
+
resolveSelection,
|
|
12
|
+
} from "./search"
|
|
13
|
+
import {
|
|
14
|
+
allocateBlockId,
|
|
15
|
+
allocateRunId,
|
|
16
|
+
applyCompressionState,
|
|
17
|
+
wrapCompressedSummary,
|
|
18
|
+
} from "./state"
|
|
19
|
+
import { finalizeSession, prepareSession, type NotificationEntry } from "./pipeline"
|
|
20
|
+
import { countTokens } from "../token-utils"
|
|
21
|
+
import { saveSessionState } from "../state/persistence"
|
|
22
|
+
import { rcFormat } from "../prompts/extensions/tool"
|
|
23
|
+
import { appendProtectedPromptInfo, appendProtectedTools } from "./protected-content"
|
|
24
|
+
import {
|
|
25
|
+
buildSummaryPrompt,
|
|
26
|
+
parseSummarySections,
|
|
27
|
+
restoreSummary,
|
|
28
|
+
type PriorSummary,
|
|
29
|
+
type SummarySection,
|
|
30
|
+
type SummarySegment,
|
|
31
|
+
} from "./summary"
|
|
32
|
+
|
|
33
|
+
export interface RcCompressToolArgs {
|
|
34
|
+
ids: string[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compress selections with a hidden, synchronous summary request.
|
|
39
|
+
*
|
|
40
|
+
* The model only selects message IDs (`ids`); summaries are generated by the
|
|
41
|
+
* plugin in a transient `session.generate` call during this tool call and are
|
|
42
|
+
* applied before the model's next step. Nothing except the `compress` tool
|
|
43
|
+
* call itself is user-visible.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
function validateArgs(args: RcCompressToolArgs): void {
|
|
47
|
+
if (!args || typeof args !== "object") {
|
|
48
|
+
throw new Error("compress requires an object argument with an `ids` array")
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(args.ids)) {
|
|
51
|
+
throw new Error("`ids` must be an array of message IDs or inclusive ranges")
|
|
52
|
+
}
|
|
53
|
+
if (args.ids.length === 0) {
|
|
54
|
+
throw new Error("`ids` must not be empty")
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface ParsedSelector {
|
|
59
|
+
selector: string
|
|
60
|
+
startRef: string
|
|
61
|
+
endRef: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function parseSelector(selector: string, format: IdFormat): ParsedSelector {
|
|
65
|
+
const raw = selector.trim().toLowerCase()
|
|
66
|
+
const parts = raw.split("-").map((part) => part.trim())
|
|
67
|
+
if (parts.length === 1 || parts.length === 2) {
|
|
68
|
+
const parseAny = (part: string) =>
|
|
69
|
+
parseBoundaryId(part, format) ??
|
|
70
|
+
parseBoundaryId(part, format === "compact" ? "xml" : "compact")
|
|
71
|
+
const toRef = (parsed: NonNullable<ReturnType<typeof parseAny>>): string =>
|
|
72
|
+
parsed.kind === "message"
|
|
73
|
+
? formatMessageRef(parsed.index, format)
|
|
74
|
+
: formatBlockRef(parsed.blockId, format)
|
|
75
|
+
|
|
76
|
+
const start = parseAny(parts[0]!)
|
|
77
|
+
const end = parts.length === 2 ? parseAny(parts[1]!) : start
|
|
78
|
+
if (start && end) {
|
|
79
|
+
if (start.kind === "message" && end.kind === "message" && start.index > end.index) {
|
|
80
|
+
throw new Error(`Invalid selector ${selector}: start comes after end`)
|
|
81
|
+
}
|
|
82
|
+
const startRef = toRef(start)
|
|
83
|
+
const endRef = toRef(end)
|
|
84
|
+
return {
|
|
85
|
+
selector: startRef === endRef ? startRef : `${startRef}-${endRef}`,
|
|
86
|
+
startRef,
|
|
87
|
+
endRef,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
throw new Error(
|
|
92
|
+
`Invalid selector ${JSON.stringify(selector)}. Use an injected message ID (e.g. ${
|
|
93
|
+
format === "compact" ? "" : "m0004"
|
|
94
|
+
}), a block ID (e.g. ${
|
|
95
|
+
format === "compact" ? "" : "b2"
|
|
96
|
+
}), or an inclusive range of either.`,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface RcPlan {
|
|
101
|
+
selector: ParsedSelector
|
|
102
|
+
messageIds: string[]
|
|
103
|
+
messages: WithParts[]
|
|
104
|
+
selection: SelectionResolution
|
|
105
|
+
anchorMessageId: string
|
|
106
|
+
priorSummaries: PriorSummary[]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function planSelections(
|
|
110
|
+
ctx: ToolContext,
|
|
111
|
+
rawMessages: WithParts[],
|
|
112
|
+
selectors: ParsedSelector[],
|
|
113
|
+
): RcPlan[] {
|
|
114
|
+
const context = buildSearchContext(ctx.state, rawMessages)
|
|
115
|
+
|
|
116
|
+
return selectors.map((selector) => {
|
|
117
|
+
const { startReference, endReference } = resolveBoundaryIds(
|
|
118
|
+
context,
|
|
119
|
+
ctx.state,
|
|
120
|
+
ctx.config,
|
|
121
|
+
selector.startRef,
|
|
122
|
+
selector.endRef,
|
|
123
|
+
)
|
|
124
|
+
const selection = resolveSelection(context, ctx.config, startReference, endReference)
|
|
125
|
+
const messages = selection.messageIds
|
|
126
|
+
.map((messageId) => context.rawMessagesById.get(messageId))
|
|
127
|
+
.filter((message): message is WithParts => Boolean(message))
|
|
128
|
+
const anchorMessageId = resolveAnchorMessageId(startReference)
|
|
129
|
+
|
|
130
|
+
const priorSummaries: PriorSummary[] = []
|
|
131
|
+
for (const blockId of selection.requiredBlockIds) {
|
|
132
|
+
const block = ctx.state.prune.messages.blocksById.get(blockId)
|
|
133
|
+
if (!block) {
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
const body = restoreSummary(block.summary)
|
|
137
|
+
if (body.length > 0) {
|
|
138
|
+
priorSummaries.push({
|
|
139
|
+
ref: formatBlockRef(blockId, ctx.state.idFormat),
|
|
140
|
+
body,
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
selector,
|
|
147
|
+
messageIds: selection.messageIds,
|
|
148
|
+
messages,
|
|
149
|
+
selection,
|
|
150
|
+
anchorMessageId,
|
|
151
|
+
priorSummaries,
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateNonOverlapping(plans: RcPlan[]): void {
|
|
157
|
+
const sorted = [...plans].sort(
|
|
158
|
+
(left, right) =>
|
|
159
|
+
left.selection.startReference.rawIndex - right.selection.startReference.rawIndex ||
|
|
160
|
+
left.selection.endReference.rawIndex - right.selection.endReference.rawIndex,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
for (let index = 1; index < sorted.length; index++) {
|
|
164
|
+
const previous = sorted[index - 1]
|
|
165
|
+
const current = sorted[index]
|
|
166
|
+
if (!previous || !current) {
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
if (current.selection.startReference.rawIndex <= previous.selection.endReference.rawIndex) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Selectors ${previous.selector.selector} and ${current.selector.selector} overlap. Overlapping selections cannot be compressed in one call.`,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function assertBlockCoverage(
|
|
178
|
+
ctx: ToolContext,
|
|
179
|
+
selector: string,
|
|
180
|
+
selection: SelectionResolution,
|
|
181
|
+
): void {
|
|
182
|
+
const required = new Set(selection.requiredBlockIds)
|
|
183
|
+
for (const messageId of selection.messageIds) {
|
|
184
|
+
const entry = ctx.state.prune.messages.byMessageId.get(messageId)
|
|
185
|
+
if (!entry || entry.activeBlockIds.length === 0) {
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
for (const blockId of entry.activeBlockIds) {
|
|
189
|
+
if (required.has(blockId)) {
|
|
190
|
+
continue
|
|
191
|
+
}
|
|
192
|
+
const ref = formatBlockRef(blockId, ctx.state.idFormat)
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Selector ${selector} only partially covers compressed block ${ref}. Include ${ref} as a boundary so its summary can be merged, or exclude its messages.`,
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function createRcCompressTool(ctx: ToolContext): ReturnType<typeof tool> {
|
|
201
|
+
ctx.prompts.reload()
|
|
202
|
+
const runtimePrompts = ctx.prompts.getRuntimePrompts()
|
|
203
|
+
return tool({
|
|
204
|
+
description: runtimePrompts.compressRc + rcFormat(ctx.state.idFormat),
|
|
205
|
+
args: {
|
|
206
|
+
ids: tool.schema
|
|
207
|
+
.array(tool.schema.string())
|
|
208
|
+
.describe(
|
|
209
|
+
"The injected message or block IDs, or inclusive ranges, to compress. Each item becomes one summary.",
|
|
210
|
+
),
|
|
211
|
+
},
|
|
212
|
+
async execute(args, toolCtx) {
|
|
213
|
+
const input = (args ?? {}) as RcCompressToolArgs
|
|
214
|
+
validateArgs(input)
|
|
215
|
+
|
|
216
|
+
const sessionID =
|
|
217
|
+
typeof (toolCtx as any)?.sessionID === "string" ? (toolCtx as any).sessionID : ""
|
|
218
|
+
if (!sessionID) {
|
|
219
|
+
throw new Error("compress could not determine the session")
|
|
220
|
+
}
|
|
221
|
+
const callId =
|
|
222
|
+
typeof (toolCtx as any)?.callID === "string" ? (toolCtx as any).callID : undefined
|
|
223
|
+
const toolMessageID = String((toolCtx as any)?.messageID ?? "")
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const { rawMessages } = await prepareSession(
|
|
227
|
+
ctx,
|
|
228
|
+
toolCtx as any,
|
|
229
|
+
`Compress: ${input.ids.join(", ")}`,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
const selectors = input.ids.map((selector) =>
|
|
233
|
+
parseSelector(selector, ctx.state.idFormat),
|
|
234
|
+
)
|
|
235
|
+
const plans = planSelections(ctx, rawMessages, selectors)
|
|
236
|
+
validateNonOverlapping(plans)
|
|
237
|
+
for (const plan of plans) {
|
|
238
|
+
assertBlockCoverage(ctx, plan.selector.selector, plan.selection)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const segments: SummarySegment[] = plans.map((plan) => ({
|
|
242
|
+
selector: plan.selector.selector,
|
|
243
|
+
messages: plan.messages,
|
|
244
|
+
priorSummaries: plan.priorSummaries,
|
|
245
|
+
}))
|
|
246
|
+
|
|
247
|
+
const text = await generateSummaries(ctx, sessionID, segments)
|
|
248
|
+
const accepted = await resolveAcceptedSections(
|
|
249
|
+
ctx,
|
|
250
|
+
plans.map((plan) => plan.selector.selector),
|
|
251
|
+
text,
|
|
252
|
+
)
|
|
253
|
+
if (accepted.length === 0) {
|
|
254
|
+
throw new Error("summary reply contained no usable sections")
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const { entries, applied, missing } = await applySummaries(
|
|
258
|
+
ctx,
|
|
259
|
+
sessionID,
|
|
260
|
+
toolMessageID,
|
|
261
|
+
callId,
|
|
262
|
+
plans,
|
|
263
|
+
accepted,
|
|
264
|
+
)
|
|
265
|
+
if (entries.length === 0) {
|
|
266
|
+
throw new Error("summary reply could not be applied to the session")
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
await finalizeSession(
|
|
270
|
+
ctx,
|
|
271
|
+
toolCtx as any,
|
|
272
|
+
rawMessages,
|
|
273
|
+
entries,
|
|
274
|
+
plans.map((plan) => plan.selector.selector).join(", "),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
if (missing.length === 0) {
|
|
278
|
+
return `Compressed: ${applied.join(", ")}.`
|
|
279
|
+
}
|
|
280
|
+
return `Compressed: ${applied.join(", ")}. Not compressed: ${missing.join(", ")}.`
|
|
281
|
+
} catch (error) {
|
|
282
|
+
await resetManualMode(ctx, sessionID)
|
|
283
|
+
throw error
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function generateSummaries(
|
|
290
|
+
ctx: ToolContext,
|
|
291
|
+
sessionID: string,
|
|
292
|
+
segments: SummarySegment[],
|
|
293
|
+
): Promise<string> {
|
|
294
|
+
const prompt = buildSummaryPrompt(segments, ctx.state.idFormat)
|
|
295
|
+
let text = await ctx.generate({ sessionID, prompt })
|
|
296
|
+
|
|
297
|
+
// One retry for sections the model failed to emit.
|
|
298
|
+
for (let retry = 0; retry < 2; retry++) {
|
|
299
|
+
const parsed = parseSummarySections(
|
|
300
|
+
text,
|
|
301
|
+
segments.map((segment) => segment.selector),
|
|
302
|
+
ctx.state.idFormat,
|
|
303
|
+
)
|
|
304
|
+
const retrySelectors = [...parsed.missing, ...parsed.empty]
|
|
305
|
+
if (retrySelectors.length === 0) {
|
|
306
|
+
break
|
|
307
|
+
}
|
|
308
|
+
const retrySegments = segments.filter((segment) =>
|
|
309
|
+
retrySelectors.includes(segment.selector.toLowerCase()),
|
|
310
|
+
)
|
|
311
|
+
if (retrySegments.length === 0) {
|
|
312
|
+
break
|
|
313
|
+
}
|
|
314
|
+
const retryPrompt = buildSummaryPrompt(retrySegments, ctx.state.idFormat)
|
|
315
|
+
try {
|
|
316
|
+
text += "\n\n" + (await ctx.generate({ sessionID, prompt: retryPrompt }))
|
|
317
|
+
} catch {
|
|
318
|
+
break
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return text
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function resolveAcceptedSections(
|
|
325
|
+
ctx: ToolContext,
|
|
326
|
+
selectors: string[],
|
|
327
|
+
text: string,
|
|
328
|
+
): Promise<SummarySection[]> {
|
|
329
|
+
const parsed = parseSummarySections(text, selectors, ctx.state.idFormat)
|
|
330
|
+
ctx.logger.info("rc summary reply parsed", {
|
|
331
|
+
replyChars: text.length,
|
|
332
|
+
accepted: parsed.sections.map((section) => section.selector).join(","),
|
|
333
|
+
missing: parsed.missing.join(","),
|
|
334
|
+
empty: parsed.empty.join(","),
|
|
335
|
+
})
|
|
336
|
+
if (parsed.missing.length > 0 || parsed.empty.length > 0) {
|
|
337
|
+
ctx.logger.warn("rc compression reply was incomplete", {
|
|
338
|
+
missing: parsed.missing.join(","),
|
|
339
|
+
empty: parsed.empty.join(","),
|
|
340
|
+
})
|
|
341
|
+
}
|
|
342
|
+
return parsed.sections
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function applySummaries(
|
|
346
|
+
ctx: ToolContext,
|
|
347
|
+
sessionID: string,
|
|
348
|
+
toolMessageID: string,
|
|
349
|
+
callId: string | undefined,
|
|
350
|
+
plans: RcPlan[],
|
|
351
|
+
sections: SummarySection[],
|
|
352
|
+
): Promise<{ entries: NotificationEntry[]; applied: string[]; missing: string[] }> {
|
|
353
|
+
// Re-read the session so we only apply summaries whose source messages are
|
|
354
|
+
// still present (a compaction or revert may have happened meanwhile).
|
|
355
|
+
const rawMessages = await fetchSessionMessages(ctx.client, sessionID)
|
|
356
|
+
const context = buildSearchContext(ctx.state, rawMessages)
|
|
357
|
+
const planBySelector = new Map(plans.map((plan) => [plan.selector.selector, plan]))
|
|
358
|
+
|
|
359
|
+
const runId = allocateRunId(ctx.state)
|
|
360
|
+
const batchTopic = plans.map((plan) => plan.selector.selector).join(", ")
|
|
361
|
+
const entries: NotificationEntry[] = []
|
|
362
|
+
const applied: string[] = []
|
|
363
|
+
const appliedSet = new Set<string>()
|
|
364
|
+
|
|
365
|
+
for (const section of sections) {
|
|
366
|
+
const plan = planBySelector.get(section.selector)
|
|
367
|
+
if (!plan) {
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
const { startReference, endReference } = resolveBoundaryIds(
|
|
373
|
+
context,
|
|
374
|
+
ctx.state,
|
|
375
|
+
ctx.config,
|
|
376
|
+
plan.selector.startRef,
|
|
377
|
+
plan.selector.endRef,
|
|
378
|
+
)
|
|
379
|
+
const selection = resolveSelection(context, ctx.config, startReference, endReference)
|
|
380
|
+
if (selection.messageIds.length === 0) {
|
|
381
|
+
continue
|
|
382
|
+
}
|
|
383
|
+
assertBlockCoverage(ctx, section.selector, selection)
|
|
384
|
+
|
|
385
|
+
const anchorMessageId = resolveAnchorMessageId(startReference)
|
|
386
|
+
const blockId = allocateBlockId(ctx.state)
|
|
387
|
+
|
|
388
|
+
const withPromptInfo = appendProtectedPromptInfo(
|
|
389
|
+
section.summary,
|
|
390
|
+
selection,
|
|
391
|
+
context,
|
|
392
|
+
ctx.state,
|
|
393
|
+
ctx.config.compress.protectTags,
|
|
394
|
+
)
|
|
395
|
+
const finalSummary = await appendProtectedTools(
|
|
396
|
+
ctx.client,
|
|
397
|
+
ctx.state,
|
|
398
|
+
ctx.config.experimental.allowSubAgents,
|
|
399
|
+
withPromptInfo,
|
|
400
|
+
selection,
|
|
401
|
+
context,
|
|
402
|
+
ctx.config.compress.protectedTools,
|
|
403
|
+
ctx.config.protectedFilePatterns,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
const storedSummary = wrapCompressedSummary(blockId, finalSummary, ctx.state.idFormat)
|
|
407
|
+
const summaryTokens = countTokens(storedSummary)
|
|
408
|
+
|
|
409
|
+
applyCompressionState(
|
|
410
|
+
ctx.state,
|
|
411
|
+
{
|
|
412
|
+
topic: section.selector,
|
|
413
|
+
batchTopic,
|
|
414
|
+
startId: plan.selector.startRef,
|
|
415
|
+
endId: plan.selector.endRef,
|
|
416
|
+
mode: "rc",
|
|
417
|
+
runId,
|
|
418
|
+
compressMessageId: toolMessageID,
|
|
419
|
+
compressCallId: callId,
|
|
420
|
+
summaryTokens,
|
|
421
|
+
},
|
|
422
|
+
selection,
|
|
423
|
+
anchorMessageId,
|
|
424
|
+
blockId,
|
|
425
|
+
storedSummary,
|
|
426
|
+
selection.requiredBlockIds,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
entries.push({
|
|
430
|
+
blockId,
|
|
431
|
+
runId,
|
|
432
|
+
summary: finalSummary,
|
|
433
|
+
summaryTokens,
|
|
434
|
+
})
|
|
435
|
+
applied.push(section.selector)
|
|
436
|
+
appliedSet.add(section.selector)
|
|
437
|
+
} catch (error) {
|
|
438
|
+
ctx.logger.warn("rc compression could not apply a summary", {
|
|
439
|
+
selector: section.selector,
|
|
440
|
+
error: error instanceof Error ? error.message : String(error),
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const missing = plans
|
|
446
|
+
.map((plan) => plan.selector.selector)
|
|
447
|
+
.filter((selector) => !appliedSet.has(selector))
|
|
448
|
+
return { entries, applied, missing }
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function resetManualMode(ctx: ToolContext, sessionID: string): Promise<void> {
|
|
452
|
+
if (ctx.state.manualMode !== "compress-pending") {
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
ctx.state.manualMode = false
|
|
456
|
+
await refreshManualMode(ctx.state, sessionID, ctx.logger, ctx.config.manualMode.enabled)
|
|
457
|
+
await saveSessionState(ctx.state, ctx.logger)
|
|
458
|
+
}
|