@tarquinen/opencode-dcp 3.2.4-beta0 → 3.2.6-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/index.ts +141 -0
- package/lib/auth.ts +37 -0
- package/lib/commands/compression-targets.ts +137 -0
- package/lib/commands/context.ts +132 -0
- package/lib/commands/decompress.ts +275 -0
- package/lib/commands/help.ts +76 -0
- package/lib/commands/index.ts +11 -0
- package/lib/commands/manual.ts +125 -0
- package/lib/commands/recompress.ts +224 -0
- package/lib/commands/stats.ts +148 -0
- package/lib/commands/sweep.ts +268 -0
- package/lib/compress-permission.ts +25 -0
- package/lib/config.ts +2 -2
- package/lib/hooks.ts +378 -0
- package/lib/host-permissions.ts +101 -0
- package/lib/messages/index.ts +8 -0
- package/lib/messages/inject/inject.ts +215 -0
- package/lib/messages/inject/subagent-results.ts +82 -0
- package/lib/messages/inject/utils.ts +374 -0
- package/lib/messages/priority.ts +102 -0
- package/lib/messages/prune.ts +238 -0
- package/lib/messages/reasoning-strip.ts +40 -0
- package/lib/messages/sync.ts +124 -0
- package/lib/messages/utils.ts +187 -0
- package/lib/prompts/compress-message.ts +42 -0
- package/lib/prompts/compress-range.ts +60 -0
- package/lib/prompts/context-limit-nudge.ts +18 -0
- package/lib/prompts/extensions/nudge.ts +43 -0
- package/lib/prompts/extensions/system.ts +32 -0
- package/lib/prompts/extensions/tool.ts +35 -0
- package/lib/prompts/index.ts +29 -0
- package/lib/prompts/iteration-nudge.ts +6 -0
- package/lib/prompts/store.ts +467 -0
- package/lib/prompts/system.ts +33 -0
- package/lib/prompts/turn-nudge.ts +10 -0
- package/lib/protected-patterns.ts +128 -0
- package/lib/strategies/deduplication.ts +127 -0
- package/lib/strategies/index.ts +2 -0
- package/lib/strategies/purge-errors.ts +88 -0
- package/lib/subagents/subagent-results.ts +74 -0
- package/lib/ui/notification.ts +346 -0
- package/lib/ui/utils.ts +287 -0
- package/package.json +14 -19
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export type PermissionAction = "ask" | "allow" | "deny"
|
|
2
|
+
|
|
3
|
+
export type PermissionValue = PermissionAction | Record<string, PermissionAction>
|
|
4
|
+
|
|
5
|
+
export type PermissionConfig = Record<string, PermissionValue> | undefined
|
|
6
|
+
|
|
7
|
+
export interface HostPermissionSnapshot {
|
|
8
|
+
global: PermissionConfig
|
|
9
|
+
agents: Record<string, PermissionConfig>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type PermissionRule = {
|
|
13
|
+
permission: string
|
|
14
|
+
pattern: string
|
|
15
|
+
action: PermissionAction
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const findLastMatchingRule = (
|
|
19
|
+
rules: PermissionRule[],
|
|
20
|
+
predicate: (rule: PermissionRule) => boolean,
|
|
21
|
+
): PermissionRule | undefined => {
|
|
22
|
+
for (let index = rules.length - 1; index >= 0; index -= 1) {
|
|
23
|
+
const rule = rules[index]
|
|
24
|
+
if (rule && predicate(rule)) {
|
|
25
|
+
return rule
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const wildcardMatch = (value: string, pattern: string): boolean => {
|
|
33
|
+
const normalizedValue = value.replaceAll("\\", "/")
|
|
34
|
+
let escaped = pattern
|
|
35
|
+
.replaceAll("\\", "/")
|
|
36
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
37
|
+
.replace(/\*/g, ".*")
|
|
38
|
+
.replace(/\?/g, ".")
|
|
39
|
+
|
|
40
|
+
if (escaped.endsWith(" .*")) {
|
|
41
|
+
escaped = escaped.slice(0, -3) + "( .*)?"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const flags = process.platform === "win32" ? "si" : "s"
|
|
45
|
+
return new RegExp(`^${escaped}$`, flags).test(normalizedValue)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const getPermissionRules = (permissionConfigs: PermissionConfig[]): PermissionRule[] => {
|
|
49
|
+
const rules: PermissionRule[] = []
|
|
50
|
+
for (const permissionConfig of permissionConfigs) {
|
|
51
|
+
if (!permissionConfig) {
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const [permission, value] of Object.entries(permissionConfig)) {
|
|
56
|
+
if (value === "ask" || value === "allow" || value === "deny") {
|
|
57
|
+
rules.push({ permission, pattern: "*", action: value })
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const [pattern, action] of Object.entries(value)) {
|
|
62
|
+
if (action === "ask" || action === "allow" || action === "deny") {
|
|
63
|
+
rules.push({ permission, pattern, action })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return rules
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const compressDisabledByOpencode = (...permissionConfigs: PermissionConfig[]): boolean => {
|
|
72
|
+
const match = findLastMatchingRule(getPermissionRules(permissionConfigs), (rule) =>
|
|
73
|
+
wildcardMatch("compress", rule.permission),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return match?.pattern === "*" && match.action === "deny"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const resolveEffectiveCompressPermission = (
|
|
80
|
+
basePermission: PermissionAction,
|
|
81
|
+
hostPermissions: HostPermissionSnapshot,
|
|
82
|
+
agentName?: string,
|
|
83
|
+
): PermissionAction => {
|
|
84
|
+
if (basePermission === "deny") {
|
|
85
|
+
return "deny"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return compressDisabledByOpencode(
|
|
89
|
+
hostPermissions.global,
|
|
90
|
+
agentName ? hostPermissions.agents[agentName] : undefined,
|
|
91
|
+
)
|
|
92
|
+
? "deny"
|
|
93
|
+
: basePermission
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const hasExplicitToolPermission = (
|
|
97
|
+
permissionConfig: PermissionConfig,
|
|
98
|
+
tool: string,
|
|
99
|
+
): boolean => {
|
|
100
|
+
return permissionConfig ? Object.prototype.hasOwnProperty.call(permissionConfig, tool) : false
|
|
101
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { prune } from "./prune"
|
|
2
|
+
export { syncCompressionBlocks } from "./sync"
|
|
3
|
+
export { injectCompressNudges } from "./inject/inject"
|
|
4
|
+
export { injectMessageIds } from "./inject/inject"
|
|
5
|
+
export { injectExtendedSubAgentResults } from "./inject/subagent-results"
|
|
6
|
+
export { stripStaleMetadata } from "./reasoning-strip"
|
|
7
|
+
export { buildPriorityMap } from "./priority"
|
|
8
|
+
export { buildToolIdList, stripHallucinations, stripHallucinationsFromString } from "./utils"
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import type { SessionState, WithParts } from "../../state"
|
|
2
|
+
import type { Logger } from "../../logger"
|
|
3
|
+
import type { PluginConfig } from "../../config"
|
|
4
|
+
import type { RuntimePrompts } from "../../prompts/store"
|
|
5
|
+
import { formatMessageIdTag } from "../../message-ids"
|
|
6
|
+
import type { CompressionPriorityMap } from "../priority"
|
|
7
|
+
import { compressPermission } from "../../compress-permission"
|
|
8
|
+
import {
|
|
9
|
+
getLastUserMessage,
|
|
10
|
+
isIgnoredUserMessage,
|
|
11
|
+
isProtectedUserMessage,
|
|
12
|
+
messageHasCompress,
|
|
13
|
+
} from "../query"
|
|
14
|
+
import { saveSessionState } from "../../state/persistence"
|
|
15
|
+
import {
|
|
16
|
+
appendToTextPart,
|
|
17
|
+
appendToLastTextPart,
|
|
18
|
+
appendToAllToolParts,
|
|
19
|
+
createSyntheticTextPart,
|
|
20
|
+
hasContent,
|
|
21
|
+
} from "../utils"
|
|
22
|
+
import {
|
|
23
|
+
addAnchor,
|
|
24
|
+
applyAnchoredNudges,
|
|
25
|
+
countMessagesAfterIndex,
|
|
26
|
+
findLastNonIgnoredMessage,
|
|
27
|
+
getIterationNudgeThreshold,
|
|
28
|
+
getNudgeFrequency,
|
|
29
|
+
getModelInfo,
|
|
30
|
+
isContextOverLimits,
|
|
31
|
+
} from "./utils"
|
|
32
|
+
|
|
33
|
+
export const injectCompressNudges = (
|
|
34
|
+
state: SessionState,
|
|
35
|
+
config: PluginConfig,
|
|
36
|
+
logger: Logger,
|
|
37
|
+
messages: WithParts[],
|
|
38
|
+
prompts: RuntimePrompts,
|
|
39
|
+
compressionPriorities?: CompressionPriorityMap,
|
|
40
|
+
): void => {
|
|
41
|
+
if (compressPermission(state, config) === "deny") {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (state.manualMode) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const lastMessage = findLastNonIgnoredMessage(messages)
|
|
50
|
+
const lastAssistantMessage = messages.findLast((message) => message.info.role === "assistant")
|
|
51
|
+
|
|
52
|
+
if (lastAssistantMessage && messageHasCompress(lastAssistantMessage)) {
|
|
53
|
+
state.nudges.contextLimitAnchors.clear()
|
|
54
|
+
state.nudges.turnNudgeAnchors.clear()
|
|
55
|
+
state.nudges.iterationNudgeAnchors.clear()
|
|
56
|
+
void saveSessionState(state, logger)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { providerId, modelId } = getModelInfo(messages)
|
|
61
|
+
let anchorsChanged = false
|
|
62
|
+
|
|
63
|
+
const { overMaxLimit, overMinLimit } = isContextOverLimits(
|
|
64
|
+
config,
|
|
65
|
+
state,
|
|
66
|
+
providerId,
|
|
67
|
+
modelId,
|
|
68
|
+
messages,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
if (!overMinLimit) {
|
|
72
|
+
const hadTurnAnchors = state.nudges.turnNudgeAnchors.size > 0
|
|
73
|
+
const hadIterationAnchors = state.nudges.iterationNudgeAnchors.size > 0
|
|
74
|
+
|
|
75
|
+
if (hadTurnAnchors || hadIterationAnchors) {
|
|
76
|
+
state.nudges.turnNudgeAnchors.clear()
|
|
77
|
+
state.nudges.iterationNudgeAnchors.clear()
|
|
78
|
+
anchorsChanged = true
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (overMaxLimit) {
|
|
83
|
+
if (lastMessage) {
|
|
84
|
+
const interval = getNudgeFrequency(config)
|
|
85
|
+
const added = addAnchor(
|
|
86
|
+
state.nudges.contextLimitAnchors,
|
|
87
|
+
lastMessage.message.info.id,
|
|
88
|
+
lastMessage.index,
|
|
89
|
+
messages,
|
|
90
|
+
interval,
|
|
91
|
+
)
|
|
92
|
+
if (added) {
|
|
93
|
+
anchorsChanged = true
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else if (overMinLimit) {
|
|
97
|
+
const isLastMessageUser = lastMessage?.message.info.role === "user"
|
|
98
|
+
|
|
99
|
+
if (isLastMessageUser && lastAssistantMessage) {
|
|
100
|
+
const previousSize = state.nudges.turnNudgeAnchors.size
|
|
101
|
+
state.nudges.turnNudgeAnchors.add(lastMessage.message.info.id)
|
|
102
|
+
state.nudges.turnNudgeAnchors.add(lastAssistantMessage.info.id)
|
|
103
|
+
if (state.nudges.turnNudgeAnchors.size !== previousSize) {
|
|
104
|
+
anchorsChanged = true
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const lastUserMessage = getLastUserMessage(messages)
|
|
109
|
+
if (lastUserMessage && lastMessage) {
|
|
110
|
+
const lastUserMessageIndex = messages.findIndex(
|
|
111
|
+
(message) => message.info.id === lastUserMessage.info.id,
|
|
112
|
+
)
|
|
113
|
+
if (lastUserMessageIndex >= 0) {
|
|
114
|
+
const messagesSinceUser = countMessagesAfterIndex(messages, lastUserMessageIndex)
|
|
115
|
+
const iterationThreshold = getIterationNudgeThreshold(config)
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
lastMessage.index > lastUserMessageIndex &&
|
|
119
|
+
messagesSinceUser >= iterationThreshold
|
|
120
|
+
) {
|
|
121
|
+
const interval = getNudgeFrequency(config)
|
|
122
|
+
const added = addAnchor(
|
|
123
|
+
state.nudges.iterationNudgeAnchors,
|
|
124
|
+
lastMessage.message.info.id,
|
|
125
|
+
lastMessage.index,
|
|
126
|
+
messages,
|
|
127
|
+
interval,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if (added) {
|
|
131
|
+
anchorsChanged = true
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
applyAnchoredNudges(state, config, messages, prompts, compressionPriorities)
|
|
139
|
+
|
|
140
|
+
if (anchorsChanged) {
|
|
141
|
+
void saveSessionState(state, logger)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const injectMessageIds = (
|
|
146
|
+
state: SessionState,
|
|
147
|
+
config: PluginConfig,
|
|
148
|
+
messages: WithParts[],
|
|
149
|
+
compressionPriorities?: CompressionPriorityMap,
|
|
150
|
+
): void => {
|
|
151
|
+
if (compressPermission(state, config) === "deny") {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const message of messages) {
|
|
156
|
+
if (isIgnoredUserMessage(message)) {
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const messageRef = state.messageIds.byRawId.get(message.info.id)
|
|
161
|
+
if (!messageRef) {
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const isBlockedMessage = isProtectedUserMessage(config, message)
|
|
166
|
+
const priority =
|
|
167
|
+
config.compress.mode === "message" && !isBlockedMessage
|
|
168
|
+
? compressionPriorities?.get(message.info.id)?.priority
|
|
169
|
+
: undefined
|
|
170
|
+
const tag = formatMessageIdTag(
|
|
171
|
+
isBlockedMessage ? "BLOCKED" : messageRef,
|
|
172
|
+
priority ? { priority } : undefined,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if (message.info.role === "user") {
|
|
176
|
+
let injected = false
|
|
177
|
+
for (const part of message.parts) {
|
|
178
|
+
if (part.type === "text") {
|
|
179
|
+
injected = appendToTextPart(part, tag) || injected
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (injected) {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
message.parts.push(createSyntheticTextPart(message, tag))
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (message.info.role !== "assistant") {
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!hasContent(message)) {
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (appendToAllToolParts(message, tag)) {
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (appendToLastTextPart(message, tag)) {
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const syntheticPart = createSyntheticTextPart(message, tag)
|
|
208
|
+
const firstToolIndex = message.parts.findIndex((p) => p.type === "tool")
|
|
209
|
+
if (firstToolIndex === -1) {
|
|
210
|
+
message.parts.push(syntheticPart)
|
|
211
|
+
} else {
|
|
212
|
+
message.parts.splice(firstToolIndex, 0, syntheticPart)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Logger } from "../../logger"
|
|
2
|
+
import type { SessionState, WithParts } from "../../state"
|
|
3
|
+
import {
|
|
4
|
+
buildSubagentResultText,
|
|
5
|
+
getSubAgentId,
|
|
6
|
+
mergeSubagentResult,
|
|
7
|
+
} from "../../subagents/subagent-results"
|
|
8
|
+
import { stripHallucinationsFromString } from "../utils"
|
|
9
|
+
|
|
10
|
+
async function fetchSubAgentMessages(client: any, sessionId: string): Promise<WithParts[]> {
|
|
11
|
+
const response = await client.session.messages({
|
|
12
|
+
path: { id: sessionId },
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const payload = (response?.data || response) as WithParts[]
|
|
16
|
+
return Array.isArray(payload) ? payload : []
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const injectExtendedSubAgentResults = async (
|
|
20
|
+
client: any,
|
|
21
|
+
state: SessionState,
|
|
22
|
+
logger: Logger,
|
|
23
|
+
messages: WithParts[],
|
|
24
|
+
allowSubAgents: boolean,
|
|
25
|
+
): Promise<void> => {
|
|
26
|
+
if (!allowSubAgents) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const message of messages) {
|
|
31
|
+
const parts = Array.isArray(message.parts) ? message.parts : []
|
|
32
|
+
|
|
33
|
+
for (const part of parts) {
|
|
34
|
+
if (part.type !== "tool" || part.tool !== "task" || !part.callID) {
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
if (state.prune.tools.has(part.callID)) {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
if (part.state?.status !== "completed" || typeof part.state.output !== "string") {
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const cachedResult = state.subAgentResultCache.get(part.callID)
|
|
45
|
+
if (cachedResult !== undefined) {
|
|
46
|
+
if (cachedResult) {
|
|
47
|
+
part.state.output = stripHallucinationsFromString(
|
|
48
|
+
mergeSubagentResult(part.state.output, cachedResult),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const subAgentSessionId = getSubAgentId(part)
|
|
55
|
+
if (!subAgentSessionId) {
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let subAgentMessages: WithParts[] = []
|
|
60
|
+
try {
|
|
61
|
+
subAgentMessages = await fetchSubAgentMessages(client, subAgentSessionId)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
logger.warn("Failed to fetch subagent session for output expansion", {
|
|
64
|
+
subAgentSessionId,
|
|
65
|
+
callID: part.callID,
|
|
66
|
+
error: error instanceof Error ? error.message : String(error),
|
|
67
|
+
})
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const subAgentResultText = buildSubagentResultText(subAgentMessages)
|
|
72
|
+
if (!subAgentResultText) {
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
state.subAgentResultCache.set(part.callID, subAgentResultText)
|
|
77
|
+
part.state.output = stripHallucinationsFromString(
|
|
78
|
+
mergeSubagentResult(part.state.output, subAgentResultText),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|