@tarquinen/opencode-dcp 3.2.4-beta0 → 3.2.5-beta0
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/dcp.schema.json +329 -0
- package/dist/lib/config.js +2 -2
- package/dist/lib/config.js.map +1 -1
- package/package.json +8 -20
- package/lib/analysis/tokens.ts +0 -225
- package/lib/compress/index.ts +0 -3
- package/lib/compress/message-utils.ts +0 -250
- package/lib/compress/message.ts +0 -137
- package/lib/compress/pipeline.ts +0 -106
- package/lib/compress/protected-content.ts +0 -154
- package/lib/compress/range-utils.ts +0 -308
- package/lib/compress/range.ts +0 -180
- package/lib/compress/search.ts +0 -267
- package/lib/compress/state.ts +0 -268
- package/lib/compress/timing.ts +0 -77
- package/lib/compress/types.ts +0 -108
- package/lib/config.ts +0 -1071
- package/lib/logger.ts +0 -235
- package/lib/message-ids.ts +0 -172
- package/lib/messages/query.ts +0 -56
- package/lib/state/index.ts +0 -4
- package/lib/state/persistence.ts +0 -256
- package/lib/state/state.ts +0 -190
- package/lib/state/tool-cache.ts +0 -98
- package/lib/state/types.ts +0 -112
- package/lib/state/utils.ts +0 -334
- package/lib/token-utils.ts +0 -162
- package/tui/data/context.ts +0 -177
- package/tui/index.tsx +0 -34
- package/tui/routes/summary.tsx +0 -175
- package/tui/shared/names.ts +0 -9
- package/tui/shared/theme.ts +0 -58
- package/tui/shared/types.ts +0 -38
- package/tui/slots/sidebar-content.tsx +0 -502
package/lib/analysis/tokens.ts
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared Token Analysis
|
|
3
|
-
* Computes a breakdown of token usage across categories for a session.
|
|
4
|
-
*
|
|
5
|
-
* TOKEN CALCULATION STRATEGY
|
|
6
|
-
* ==========================
|
|
7
|
-
* We minimize tokenizer estimation by leveraging API-reported values wherever possible.
|
|
8
|
-
*
|
|
9
|
-
* WHAT WE GET FROM THE API (exact):
|
|
10
|
-
* - tokens.input : Input tokens for each assistant response
|
|
11
|
-
* - tokens.output : Output tokens generated (includes text + tool calls)
|
|
12
|
-
* - tokens.reasoning: Reasoning tokens used
|
|
13
|
-
* - tokens.cache : Cache read/write tokens
|
|
14
|
-
*
|
|
15
|
-
* HOW WE CALCULATE EACH CATEGORY:
|
|
16
|
-
*
|
|
17
|
-
* SYSTEM = firstAssistant.input + cache.read + cache.write - tokenizer(firstUserMessage)
|
|
18
|
-
* The first response's total input (input + cache.read + cache.write)
|
|
19
|
-
* contains system + first user message. On the first request of a
|
|
20
|
-
* session, the system prompt appears in cache.write (cache creation),
|
|
21
|
-
* not cache.read.
|
|
22
|
-
*
|
|
23
|
-
* TOOLS = tokenizer(toolInputs + toolOutputs) - prunedTokens
|
|
24
|
-
* We must tokenize tools anyway for pruning decisions.
|
|
25
|
-
*
|
|
26
|
-
* USER = tokenizer(all user messages)
|
|
27
|
-
* User messages are typically small, so estimation is acceptable.
|
|
28
|
-
*
|
|
29
|
-
* ASSISTANT = total - system - user - tools
|
|
30
|
-
* Calculated as residual. This absorbs:
|
|
31
|
-
* - Assistant text output tokens
|
|
32
|
-
* - Reasoning tokens (if persisted by the model)
|
|
33
|
-
* - Any estimation errors
|
|
34
|
-
*
|
|
35
|
-
* TOTAL = input + output + reasoning + cache.read + cache.write
|
|
36
|
-
* Matches opencode's UI display.
|
|
37
|
-
*
|
|
38
|
-
* WHY ASSISTANT IS THE RESIDUAL:
|
|
39
|
-
* If reasoning tokens persist in context (model-dependent), they semantically
|
|
40
|
-
* belong with "Assistant" since reasoning IS assistant-generated content.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
import type { AssistantMessage, TextPart, ToolPart } from "@opencode-ai/sdk/v2"
|
|
44
|
-
import type { SessionState, WithParts } from "../state"
|
|
45
|
-
import { isIgnoredUserMessage } from "../messages/query"
|
|
46
|
-
import { isMessageCompacted } from "../state/utils"
|
|
47
|
-
import { countTokens, extractCompletedToolOutput } from "../token-utils"
|
|
48
|
-
|
|
49
|
-
export type MessageStatus = "active" | "pruned"
|
|
50
|
-
|
|
51
|
-
export interface TokenBreakdown {
|
|
52
|
-
system: number
|
|
53
|
-
user: number
|
|
54
|
-
assistant: number
|
|
55
|
-
tools: number
|
|
56
|
-
toolCount: number
|
|
57
|
-
toolsInContextCount: number
|
|
58
|
-
prunedTokens: number
|
|
59
|
-
prunedToolCount: number
|
|
60
|
-
prunedMessageCount: number
|
|
61
|
-
total: number
|
|
62
|
-
messageCount: number
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface TokenAnalysis {
|
|
66
|
-
breakdown: TokenBreakdown
|
|
67
|
-
messageStatuses: MessageStatus[]
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function emptyBreakdown(): TokenBreakdown {
|
|
71
|
-
return {
|
|
72
|
-
system: 0,
|
|
73
|
-
user: 0,
|
|
74
|
-
assistant: 0,
|
|
75
|
-
tools: 0,
|
|
76
|
-
toolCount: 0,
|
|
77
|
-
toolsInContextCount: 0,
|
|
78
|
-
prunedTokens: 0,
|
|
79
|
-
prunedToolCount: 0,
|
|
80
|
-
prunedMessageCount: 0,
|
|
81
|
-
total: 0,
|
|
82
|
-
messageCount: 0,
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function analyzeTokens(state: SessionState, messages: WithParts[]): TokenAnalysis {
|
|
87
|
-
const breakdown = emptyBreakdown()
|
|
88
|
-
const messageStatuses: MessageStatus[] = []
|
|
89
|
-
breakdown.prunedTokens = state.stats.totalPruneTokens
|
|
90
|
-
|
|
91
|
-
let firstAssistant: AssistantMessage | undefined
|
|
92
|
-
for (const msg of messages) {
|
|
93
|
-
if (msg.info.role !== "assistant") continue
|
|
94
|
-
const assistantInfo = msg.info as AssistantMessage
|
|
95
|
-
if (
|
|
96
|
-
assistantInfo.tokens?.input > 0 ||
|
|
97
|
-
assistantInfo.tokens?.cache?.read > 0 ||
|
|
98
|
-
assistantInfo.tokens?.cache?.write > 0
|
|
99
|
-
) {
|
|
100
|
-
firstAssistant = assistantInfo
|
|
101
|
-
break
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let lastAssistant: AssistantMessage | undefined
|
|
106
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
107
|
-
const msg = messages[i]
|
|
108
|
-
if (msg.info.role !== "assistant") continue
|
|
109
|
-
const assistantInfo = msg.info as AssistantMessage
|
|
110
|
-
if (assistantInfo.tokens?.output > 0) {
|
|
111
|
-
lastAssistant = assistantInfo
|
|
112
|
-
break
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const apiInput = lastAssistant?.tokens?.input || 0
|
|
117
|
-
const apiOutput = lastAssistant?.tokens?.output || 0
|
|
118
|
-
const apiReasoning = lastAssistant?.tokens?.reasoning || 0
|
|
119
|
-
const apiCacheRead = lastAssistant?.tokens?.cache?.read || 0
|
|
120
|
-
const apiCacheWrite = lastAssistant?.tokens?.cache?.write || 0
|
|
121
|
-
breakdown.total = apiInput + apiOutput + apiReasoning + apiCacheRead + apiCacheWrite
|
|
122
|
-
|
|
123
|
-
const userTextParts: string[] = []
|
|
124
|
-
const toolInputParts: string[] = []
|
|
125
|
-
const toolOutputParts: string[] = []
|
|
126
|
-
const allToolIds = new Set<string>()
|
|
127
|
-
const activeToolIds = new Set<string>()
|
|
128
|
-
const prunedByMessageToolIds = new Set<string>()
|
|
129
|
-
const allMessageIds = new Set<string>()
|
|
130
|
-
|
|
131
|
-
let firstUserText = ""
|
|
132
|
-
let foundFirstUser = false
|
|
133
|
-
|
|
134
|
-
for (const msg of messages) {
|
|
135
|
-
const ignoredUser = msg.info.role === "user" && isIgnoredUserMessage(msg)
|
|
136
|
-
if (ignoredUser) continue
|
|
137
|
-
|
|
138
|
-
allMessageIds.add(msg.info.id)
|
|
139
|
-
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
140
|
-
const compacted = isMessageCompacted(state, msg)
|
|
141
|
-
const pruneEntry = state.prune.messages.byMessageId.get(msg.info.id)
|
|
142
|
-
const messagePruned = !!pruneEntry && pruneEntry.activeBlockIds.length > 0
|
|
143
|
-
const messageActive = !compacted && !messagePruned
|
|
144
|
-
|
|
145
|
-
breakdown.messageCount += 1
|
|
146
|
-
messageStatuses.push(messageActive ? "active" : "pruned")
|
|
147
|
-
|
|
148
|
-
for (const part of parts) {
|
|
149
|
-
if (part.type === "tool") {
|
|
150
|
-
const toolPart = part as ToolPart
|
|
151
|
-
if (toolPart.callID) {
|
|
152
|
-
allToolIds.add(toolPart.callID)
|
|
153
|
-
if (!compacted) activeToolIds.add(toolPart.callID)
|
|
154
|
-
if (messagePruned) prunedByMessageToolIds.add(toolPart.callID)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const toolPruned = toolPart.callID && state.prune.tools.has(toolPart.callID)
|
|
158
|
-
if (!compacted && !toolPruned) {
|
|
159
|
-
if (toolPart.state?.input) {
|
|
160
|
-
const inputText =
|
|
161
|
-
typeof toolPart.state.input === "string"
|
|
162
|
-
? toolPart.state.input
|
|
163
|
-
: JSON.stringify(toolPart.state.input)
|
|
164
|
-
toolInputParts.push(inputText)
|
|
165
|
-
}
|
|
166
|
-
const outputText = extractCompletedToolOutput(toolPart)
|
|
167
|
-
if (outputText !== undefined) {
|
|
168
|
-
toolOutputParts.push(outputText)
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
continue
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (part.type === "text" && msg.info.role === "user" && !compacted) {
|
|
175
|
-
const textPart = part as TextPart
|
|
176
|
-
const text = textPart.text || ""
|
|
177
|
-
userTextParts.push(text)
|
|
178
|
-
if (!foundFirstUser) firstUserText += text
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (msg.info.role === "user" && !foundFirstUser) {
|
|
183
|
-
foundFirstUser = true
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const prunedByToolIds = new Set<string>()
|
|
188
|
-
for (const toolID of allToolIds) {
|
|
189
|
-
if (state.prune.tools.has(toolID)) prunedByToolIds.add(toolID)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const prunedToolIds = new Set<string>([...prunedByToolIds, ...prunedByMessageToolIds])
|
|
193
|
-
breakdown.toolCount = allToolIds.size
|
|
194
|
-
breakdown.toolsInContextCount = [...activeToolIds].filter(
|
|
195
|
-
(id) => !prunedByToolIds.has(id),
|
|
196
|
-
).length
|
|
197
|
-
breakdown.prunedToolCount = prunedToolIds.size
|
|
198
|
-
|
|
199
|
-
for (const [messageID, entry] of state.prune.messages.byMessageId) {
|
|
200
|
-
if (allMessageIds.has(messageID) && entry.activeBlockIds.length > 0) {
|
|
201
|
-
breakdown.prunedMessageCount += 1
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const firstUserTokens = countTokens(firstUserText)
|
|
206
|
-
breakdown.user = countTokens(userTextParts.join("\n"))
|
|
207
|
-
const toolInputTokens = countTokens(toolInputParts.join("\n"))
|
|
208
|
-
const toolOutputTokens = countTokens(toolOutputParts.join("\n"))
|
|
209
|
-
|
|
210
|
-
if (firstAssistant) {
|
|
211
|
-
const firstInput =
|
|
212
|
-
(firstAssistant.tokens?.input || 0) +
|
|
213
|
-
(firstAssistant.tokens?.cache?.read || 0) +
|
|
214
|
-
(firstAssistant.tokens?.cache?.write || 0)
|
|
215
|
-
breakdown.system = Math.max(0, firstInput - firstUserTokens)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
breakdown.tools = toolInputTokens + toolOutputTokens
|
|
219
|
-
breakdown.assistant = Math.max(
|
|
220
|
-
0,
|
|
221
|
-
breakdown.total - breakdown.system - breakdown.user - breakdown.tools,
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
return { breakdown, messageStatuses }
|
|
225
|
-
}
|
package/lib/compress/index.ts
DELETED
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
import type { PluginConfig } from "../config"
|
|
2
|
-
import type { SessionState } from "../state"
|
|
3
|
-
import { parseBoundaryId } from "../message-ids"
|
|
4
|
-
import { isIgnoredUserMessage, isProtectedUserMessage } from "../messages/query"
|
|
5
|
-
import { resolveAnchorMessageId, resolveBoundaryIds, resolveSelection } from "./search"
|
|
6
|
-
import { COMPRESSED_BLOCK_HEADER } from "./state"
|
|
7
|
-
import type {
|
|
8
|
-
CompressMessageEntry,
|
|
9
|
-
CompressMessageToolArgs,
|
|
10
|
-
ResolvedMessageCompression,
|
|
11
|
-
ResolvedMessageCompressionsResult,
|
|
12
|
-
SearchContext,
|
|
13
|
-
} from "./types"
|
|
14
|
-
|
|
15
|
-
interface SkippedIssue {
|
|
16
|
-
kind: string
|
|
17
|
-
messageId: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
class SoftIssue extends Error {
|
|
21
|
-
constructor(
|
|
22
|
-
public readonly kind: string,
|
|
23
|
-
public readonly messageId: string,
|
|
24
|
-
message: string,
|
|
25
|
-
) {
|
|
26
|
-
super(message)
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function validateArgs(args: CompressMessageToolArgs): void {
|
|
31
|
-
if (typeof args.topic !== "string" || args.topic.trim().length === 0) {
|
|
32
|
-
throw new Error("topic is required and must be a non-empty string")
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (!Array.isArray(args.content) || args.content.length === 0) {
|
|
36
|
-
throw new Error("content is required and must be a non-empty array")
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
for (let index = 0; index < args.content.length; index++) {
|
|
40
|
-
const entry = args.content[index]
|
|
41
|
-
const prefix = `content[${index}]`
|
|
42
|
-
|
|
43
|
-
if (typeof entry?.messageId !== "string" || entry.messageId.trim().length === 0) {
|
|
44
|
-
throw new Error(`${prefix}.messageId is required and must be a non-empty string`)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (typeof entry?.topic !== "string" || entry.topic.trim().length === 0) {
|
|
48
|
-
throw new Error(`${prefix}.topic is required and must be a non-empty string`)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (typeof entry?.summary !== "string" || entry.summary.trim().length === 0) {
|
|
52
|
-
throw new Error(`${prefix}.summary is required and must be a non-empty string`)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function formatResult(
|
|
58
|
-
processedCount: number,
|
|
59
|
-
skippedIssues: string[],
|
|
60
|
-
skippedCount: number,
|
|
61
|
-
): string {
|
|
62
|
-
const messageNoun = processedCount === 1 ? "message" : "messages"
|
|
63
|
-
const processedText =
|
|
64
|
-
processedCount > 0
|
|
65
|
-
? `Compressed ${processedCount} ${messageNoun} into ${COMPRESSED_BLOCK_HEADER}.`
|
|
66
|
-
: "Compressed 0 messages."
|
|
67
|
-
|
|
68
|
-
if (skippedCount === 0) {
|
|
69
|
-
return processedText
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const issueNoun = skippedCount === 1 ? "issue" : "issues"
|
|
73
|
-
const issueLines = skippedIssues.map((issue) => `- ${issue}`).join("\n")
|
|
74
|
-
return `${processedText}\nSkipped ${skippedCount} ${issueNoun}:\n${issueLines}`
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export function formatIssues(skippedIssues: string[], skippedCount: number): string {
|
|
78
|
-
const issueNoun = skippedCount === 1 ? "issue" : "issues"
|
|
79
|
-
const issueLines = skippedIssues.map((issue) => `- ${issue}`).join("\n")
|
|
80
|
-
return `Unable to compress any messages. Found ${skippedCount} ${issueNoun}:\n${issueLines}`
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const ISSUE_TEMPLATES: Record<string, [singular: string, plural: string]> = {
|
|
84
|
-
blocked: [
|
|
85
|
-
"refers to a protected message and cannot be compressed.",
|
|
86
|
-
"refer to protected messages and cannot be compressed.",
|
|
87
|
-
],
|
|
88
|
-
"invalid-format": [
|
|
89
|
-
"is invalid. Use an injected raw message ID of the form mNNNN.",
|
|
90
|
-
"are invalid. Use injected raw message IDs of the form mNNNN.",
|
|
91
|
-
],
|
|
92
|
-
"block-id": [
|
|
93
|
-
"is invalid here. Block IDs like bN are not allowed; use an mNNNN message ID instead.",
|
|
94
|
-
"are invalid here. Block IDs like bN are not allowed; use mNNNN message IDs instead.",
|
|
95
|
-
],
|
|
96
|
-
"not-in-context": [
|
|
97
|
-
"is not available in the current conversation context. Choose an injected mNNNN ID visible in context.",
|
|
98
|
-
"are not available in the current conversation context. Choose injected mNNNN IDs visible in context.",
|
|
99
|
-
],
|
|
100
|
-
protected: [
|
|
101
|
-
"refers to a protected message and cannot be compressed.",
|
|
102
|
-
"refer to protected messages and cannot be compressed.",
|
|
103
|
-
],
|
|
104
|
-
"already-compressed": [
|
|
105
|
-
"is already part of an active compression.",
|
|
106
|
-
"are already part of active compressions.",
|
|
107
|
-
],
|
|
108
|
-
duplicate: [
|
|
109
|
-
"was selected more than once in this batch.",
|
|
110
|
-
"were each selected more than once in this batch.",
|
|
111
|
-
],
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function formatSkippedGroup(kind: string, messageIds: string[]): string {
|
|
115
|
-
const templates = ISSUE_TEMPLATES[kind]
|
|
116
|
-
const ids = messageIds.join(", ")
|
|
117
|
-
const single = messageIds.length === 1
|
|
118
|
-
const prefix = single ? "messageId" : "messageIds"
|
|
119
|
-
|
|
120
|
-
if (!templates) {
|
|
121
|
-
return `${prefix} ${ids}: unknown issue.`
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return `${prefix} ${ids} ${single ? templates[0] : templates[1]}`
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function groupSkippedIssues(issues: SkippedIssue[]): string[] {
|
|
128
|
-
const groups = new Map<string, string[]>()
|
|
129
|
-
const order: string[] = []
|
|
130
|
-
|
|
131
|
-
for (const issue of issues) {
|
|
132
|
-
let ids = groups.get(issue.kind)
|
|
133
|
-
if (!ids) {
|
|
134
|
-
ids = []
|
|
135
|
-
groups.set(issue.kind, ids)
|
|
136
|
-
order.push(issue.kind)
|
|
137
|
-
}
|
|
138
|
-
ids.push(issue.messageId)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return order.map((kind) => {
|
|
142
|
-
const ids = groups.get(kind)!
|
|
143
|
-
return formatSkippedGroup(kind, ids)
|
|
144
|
-
})
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function resolveMessages(
|
|
148
|
-
args: CompressMessageToolArgs,
|
|
149
|
-
searchContext: SearchContext,
|
|
150
|
-
state: SessionState,
|
|
151
|
-
config: PluginConfig,
|
|
152
|
-
): ResolvedMessageCompressionsResult {
|
|
153
|
-
const issues: SkippedIssue[] = []
|
|
154
|
-
const plans: ResolvedMessageCompression[] = []
|
|
155
|
-
const seenMessageIds = new Set<string>()
|
|
156
|
-
|
|
157
|
-
for (const entry of args.content) {
|
|
158
|
-
const normalizedMessageId = entry.messageId.trim()
|
|
159
|
-
if (seenMessageIds.has(normalizedMessageId)) {
|
|
160
|
-
issues.push({ kind: "duplicate", messageId: normalizedMessageId })
|
|
161
|
-
continue
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
const plan = resolveMessage(
|
|
166
|
-
{
|
|
167
|
-
...entry,
|
|
168
|
-
messageId: normalizedMessageId,
|
|
169
|
-
},
|
|
170
|
-
searchContext,
|
|
171
|
-
state,
|
|
172
|
-
config,
|
|
173
|
-
)
|
|
174
|
-
seenMessageIds.add(plan.entry.messageId)
|
|
175
|
-
plans.push(plan)
|
|
176
|
-
} catch (error: any) {
|
|
177
|
-
if (error instanceof SoftIssue) {
|
|
178
|
-
issues.push({ kind: error.kind, messageId: error.messageId })
|
|
179
|
-
continue
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
throw error
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
plans,
|
|
188
|
-
skippedIssues: groupSkippedIssues(issues),
|
|
189
|
-
skippedCount: issues.length,
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function resolveMessage(
|
|
194
|
-
entry: CompressMessageEntry,
|
|
195
|
-
searchContext: SearchContext,
|
|
196
|
-
state: SessionState,
|
|
197
|
-
config: PluginConfig,
|
|
198
|
-
): ResolvedMessageCompression {
|
|
199
|
-
if (entry.messageId.toUpperCase() === "BLOCKED") {
|
|
200
|
-
throw new SoftIssue("blocked", "BLOCKED", "protected message")
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const parsed = parseBoundaryId(entry.messageId)
|
|
204
|
-
|
|
205
|
-
if (!parsed) {
|
|
206
|
-
throw new SoftIssue("invalid-format", entry.messageId, "invalid format")
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (parsed.kind === "compressed-block") {
|
|
210
|
-
throw new SoftIssue("block-id", entry.messageId, "block ID used")
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const messageId = state.messageIds.byRef.get(parsed.ref)
|
|
214
|
-
const rawMessage = messageId ? searchContext.rawMessagesById.get(messageId) : undefined
|
|
215
|
-
if (
|
|
216
|
-
!messageId ||
|
|
217
|
-
!rawMessage ||
|
|
218
|
-
!searchContext.rawIndexById.has(messageId) ||
|
|
219
|
-
isIgnoredUserMessage(rawMessage)
|
|
220
|
-
) {
|
|
221
|
-
throw new SoftIssue("not-in-context", parsed.ref, "not in context")
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const { startReference, endReference } = resolveBoundaryIds(
|
|
225
|
-
searchContext,
|
|
226
|
-
state,
|
|
227
|
-
parsed.ref,
|
|
228
|
-
parsed.ref,
|
|
229
|
-
)
|
|
230
|
-
const selection = resolveSelection(searchContext, startReference, endReference)
|
|
231
|
-
|
|
232
|
-
if (isProtectedUserMessage(config, rawMessage)) {
|
|
233
|
-
throw new SoftIssue("protected", parsed.ref, "protected message")
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const pruneEntry = state.prune.messages.byMessageId.get(messageId)
|
|
237
|
-
if (pruneEntry && pruneEntry.activeBlockIds.length > 0) {
|
|
238
|
-
throw new SoftIssue("already-compressed", parsed.ref, "already compressed")
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return {
|
|
242
|
-
entry: {
|
|
243
|
-
messageId: parsed.ref,
|
|
244
|
-
topic: entry.topic,
|
|
245
|
-
summary: entry.summary,
|
|
246
|
-
},
|
|
247
|
-
selection,
|
|
248
|
-
anchorMessageId: resolveAnchorMessageId(startReference),
|
|
249
|
-
}
|
|
250
|
-
}
|
package/lib/compress/message.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin"
|
|
2
|
-
import type { ToolContext } from "./types"
|
|
3
|
-
import { countTokens } from "../token-utils"
|
|
4
|
-
import { MESSAGE_FORMAT_EXTENSION } from "../prompts/extensions/tool"
|
|
5
|
-
import { formatIssues, formatResult, resolveMessages, validateArgs } from "./message-utils"
|
|
6
|
-
import { finalizeSession, prepareSession, type NotificationEntry } from "./pipeline"
|
|
7
|
-
import { appendProtectedTools } from "./protected-content"
|
|
8
|
-
import {
|
|
9
|
-
allocateBlockId,
|
|
10
|
-
allocateRunId,
|
|
11
|
-
applyCompressionState,
|
|
12
|
-
wrapCompressedSummary,
|
|
13
|
-
} from "./state"
|
|
14
|
-
import type { CompressMessageToolArgs } from "./types"
|
|
15
|
-
|
|
16
|
-
function buildSchema() {
|
|
17
|
-
return {
|
|
18
|
-
topic: tool.schema
|
|
19
|
-
.string()
|
|
20
|
-
.describe(
|
|
21
|
-
"Short label (3-5 words) for the overall batch - e.g., 'Closed Research Notes'",
|
|
22
|
-
),
|
|
23
|
-
content: tool.schema
|
|
24
|
-
.array(
|
|
25
|
-
tool.schema.object({
|
|
26
|
-
messageId: tool.schema
|
|
27
|
-
.string()
|
|
28
|
-
.describe("Raw message ID to compress (e.g. m0001)"),
|
|
29
|
-
topic: tool.schema
|
|
30
|
-
.string()
|
|
31
|
-
.describe("Short label (3-5 words) for this one message summary"),
|
|
32
|
-
summary: tool.schema
|
|
33
|
-
.string()
|
|
34
|
-
.describe("Complete technical summary replacing that one message"),
|
|
35
|
-
}),
|
|
36
|
-
)
|
|
37
|
-
.describe("Batch of individual message summaries to create in one tool call"),
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function createCompressMessageTool(ctx: ToolContext): ReturnType<typeof tool> {
|
|
42
|
-
ctx.prompts.reload()
|
|
43
|
-
const runtimePrompts = ctx.prompts.getRuntimePrompts()
|
|
44
|
-
|
|
45
|
-
return tool({
|
|
46
|
-
description: runtimePrompts.compressMessage + MESSAGE_FORMAT_EXTENSION,
|
|
47
|
-
args: buildSchema(),
|
|
48
|
-
async execute(args, toolCtx) {
|
|
49
|
-
const input = args as CompressMessageToolArgs
|
|
50
|
-
validateArgs(input)
|
|
51
|
-
const callId =
|
|
52
|
-
typeof (toolCtx as unknown as { callID?: unknown }).callID === "string"
|
|
53
|
-
? (toolCtx as unknown as { callID: string }).callID
|
|
54
|
-
: undefined
|
|
55
|
-
|
|
56
|
-
const { rawMessages, searchContext } = await prepareSession(
|
|
57
|
-
ctx,
|
|
58
|
-
toolCtx,
|
|
59
|
-
`Compress Message: ${input.topic}`,
|
|
60
|
-
)
|
|
61
|
-
const { plans, skippedIssues, skippedCount } = resolveMessages(
|
|
62
|
-
input,
|
|
63
|
-
searchContext,
|
|
64
|
-
ctx.state,
|
|
65
|
-
ctx.config,
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
if (plans.length === 0 && skippedCount > 0) {
|
|
69
|
-
throw new Error(formatIssues(skippedIssues, skippedCount))
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const notifications: NotificationEntry[] = []
|
|
73
|
-
|
|
74
|
-
const preparedPlans: Array<{
|
|
75
|
-
plan: (typeof plans)[number]
|
|
76
|
-
summaryWithTools: string
|
|
77
|
-
}> = []
|
|
78
|
-
|
|
79
|
-
for (const plan of plans) {
|
|
80
|
-
const summaryWithTools = await appendProtectedTools(
|
|
81
|
-
ctx.client,
|
|
82
|
-
ctx.state,
|
|
83
|
-
ctx.config.experimental.allowSubAgents,
|
|
84
|
-
plan.entry.summary,
|
|
85
|
-
plan.selection,
|
|
86
|
-
searchContext,
|
|
87
|
-
ctx.config.compress.protectedTools,
|
|
88
|
-
ctx.config.protectedFilePatterns,
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
preparedPlans.push({
|
|
92
|
-
plan,
|
|
93
|
-
summaryWithTools,
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const runId = allocateRunId(ctx.state)
|
|
98
|
-
|
|
99
|
-
for (const { plan, summaryWithTools } of preparedPlans) {
|
|
100
|
-
const blockId = allocateBlockId(ctx.state)
|
|
101
|
-
const storedSummary = wrapCompressedSummary(blockId, summaryWithTools)
|
|
102
|
-
const summaryTokens = countTokens(storedSummary)
|
|
103
|
-
|
|
104
|
-
applyCompressionState(
|
|
105
|
-
ctx.state,
|
|
106
|
-
{
|
|
107
|
-
topic: plan.entry.topic,
|
|
108
|
-
batchTopic: input.topic,
|
|
109
|
-
startId: plan.entry.messageId,
|
|
110
|
-
endId: plan.entry.messageId,
|
|
111
|
-
mode: "message",
|
|
112
|
-
runId,
|
|
113
|
-
compressMessageId: toolCtx.messageID,
|
|
114
|
-
compressCallId: callId,
|
|
115
|
-
summaryTokens,
|
|
116
|
-
},
|
|
117
|
-
plan.selection,
|
|
118
|
-
plan.anchorMessageId,
|
|
119
|
-
blockId,
|
|
120
|
-
storedSummary,
|
|
121
|
-
[],
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
notifications.push({
|
|
125
|
-
blockId,
|
|
126
|
-
runId,
|
|
127
|
-
summary: summaryWithTools,
|
|
128
|
-
summaryTokens,
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
await finalizeSession(ctx, toolCtx, rawMessages, notifications, input.topic)
|
|
133
|
-
|
|
134
|
-
return formatResult(plans.length, skippedIssues, skippedCount)
|
|
135
|
-
},
|
|
136
|
-
})
|
|
137
|
-
}
|