Skip to content

Commit 2c28051

Browse files
committed
refactor(prompts): reorganize extensions and add protected tools extension
1 parent 68a094b commit 2c28051

15 files changed

Lines changed: 154 additions & 131 deletions

File tree

lib/commands/manual.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { SessionState, WithParts } from "../state"
1212
import type { PluginConfig } from "../config"
1313
import { sendIgnoredMessage } from "../ui/notification"
1414
import { getCurrentParams } from "../token-utils"
15-
import { buildCompressedBlockGuidance } from "../messages/inject/utils"
15+
import { buildCompressedBlockGuidance } from "../prompts/extensions/nudge"
1616
import { isIgnoredUserMessage } from "../messages/query"
1717

1818
const MANUAL_MODE_ON = "Manual mode is now ON. Use /dcp compress to trigger context tools manually."

lib/compress/message.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { tool } from "@opencode-ai/plugin"
22
import type { ToolContext } from "./types"
33
import { countTokens } from "../token-utils"
4-
import { MESSAGE_FORMAT_OVERLAY } from "../prompts/internal-overlays"
4+
import { MESSAGE_FORMAT_EXTENSION } from "../prompts/extensions/tool"
55
import { formatIssues, formatResult, resolveMessages, validateArgs } from "./message-utils"
66
import { finalizeSession, prepareSession, type NotificationEntry } from "./pipeline"
77
import { appendProtectedTools } from "./protected-content"
@@ -43,7 +43,7 @@ export function createCompressMessageTool(ctx: ToolContext): ReturnType<typeof t
4343
const runtimePrompts = ctx.prompts.getRuntimePrompts()
4444

4545
return tool({
46-
description: runtimePrompts.compressMessage + MESSAGE_FORMAT_OVERLAY,
46+
description: runtimePrompts.compressMessage + MESSAGE_FORMAT_EXTENSION,
4747
args: buildSchema(),
4848
async execute(args, toolCtx) {
4949
const input = args as CompressMessageToolArgs

lib/compress/range.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { tool } from "@opencode-ai/plugin"
22
import type { ToolContext } from "./types"
33
import { countTokens } from "../token-utils"
4-
import { RANGE_FORMAT_OVERLAY } from "../prompts/internal-overlays"
4+
import { RANGE_FORMAT_EXTENSION } from "../prompts/extensions/tool"
55
import { finalizeSession, prepareSession, type NotificationEntry } from "./pipeline"
66
import { appendProtectedTools, appendProtectedUserMessages } from "./protected-content"
77
import {
@@ -54,7 +54,7 @@ export function createCompressRangeTool(ctx: ToolContext): ReturnType<typeof too
5454
const runtimePrompts = ctx.prompts.getRuntimePrompts()
5555

5656
return tool({
57-
description: runtimePrompts.compressRange + RANGE_FORMAT_OVERLAY,
57+
description: runtimePrompts.compressRange + RANGE_FORMAT_EXTENSION,
5858
args: buildSchema(),
5959
async execute(args, toolCtx) {
6060
const input = args as CompressRangeToolArgs

lib/hooks.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
syncCompressionBlocks,
1616
} from "./messages"
1717
import { renderSystemPrompt, type PromptStore } from "./prompts"
18+
import { buildProtectedToolsExtension } from "./prompts/extensions/system"
1819
import {
1920
applyPendingManualTrigger,
2021
handleContextCommand,
@@ -75,6 +76,7 @@ export function createSystemPromptHandler(
7576
const runtimePrompts = prompts.getRuntimePrompts()
7677
const newPrompt = renderSystemPrompt(
7778
runtimePrompts,
79+
buildProtectedToolsExtension(config.compress.protectedTools),
7880
!!state.manualMode,
7981
state.isSubAgent && config.experimental.allowSubAgents,
8082
)

lib/messages/inject/utils.ts

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { SessionState, WithParts } from "../../state"
22
import type { PluginConfig } from "../../config"
3-
import { renderMessagePriorityGuidance } from "../../prompts/message-priority-guidance"
3+
import {
4+
appendGuidanceToDcpTag,
5+
buildCompressedBlockGuidance,
6+
renderMessagePriorityGuidance,
7+
} from "../../prompts/extensions/nudge"
48
import type { RuntimePrompts } from "../../prompts/store"
59
import type { UserMessage } from "@opencode-ai/sdk/v2"
610
import {
@@ -188,38 +192,6 @@ export function addAnchor(
188192
return anchorMessageIds.size !== previousSize
189193
}
190194

191-
export function buildCompressedBlockGuidance(state: SessionState): string {
192-
const refs = Array.from(state.prune.messages.activeBlockIds)
193-
.filter((id) => Number.isInteger(id) && id > 0)
194-
.sort((a, b) => a - b)
195-
.map((id) => `b${id}`)
196-
const blockCount = refs.length
197-
const blockList = blockCount > 0 ? refs.join(", ") : "none"
198-
199-
return [
200-
"Compressed block context:",
201-
`- Active compressed blocks in this session: ${blockCount} (${blockList})`,
202-
"- If your selected compression range includes any listed block, include each required placeholder exactly once in the summary using \`(bN)\`.",
203-
].join("\n")
204-
}
205-
206-
function appendGuidanceToDcpTag(nudgeText: string, guidance: string): string {
207-
if (!guidance.trim()) {
208-
return nudgeText
209-
}
210-
211-
const closeTag = "</dcp-system-reminder>"
212-
const closeTagIndex = nudgeText.lastIndexOf(closeTag)
213-
214-
if (closeTagIndex === -1) {
215-
return nudgeText
216-
}
217-
218-
const beforeClose = nudgeText.slice(0, closeTagIndex).trimEnd()
219-
const afterClose = nudgeText.slice(closeTagIndex)
220-
return `${beforeClose}\n\n${guidance}\n${afterClose}`
221-
}
222-
223195
function buildMessagePriorityGuidance(
224196
messages: WithParts[],
225197
compressionPriorities: CompressionPriorityMap | undefined,

lib/prompts/extensions/nudge.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { SessionState } from "../../state"
2+
3+
export function buildCompressedBlockGuidance(state: SessionState): string {
4+
const refs = Array.from(state.prune.messages.activeBlockIds)
5+
.filter((id) => Number.isInteger(id) && id > 0)
6+
.sort((a, b) => a - b)
7+
.map((id) => `b${id}`)
8+
const blockCount = refs.length
9+
const blockList = blockCount > 0 ? refs.join(", ") : "none"
10+
11+
return [
12+
"Compressed block context:",
13+
`- Active compressed blocks in this session: ${blockCount} (${blockList})`,
14+
"- If your selected compression range includes any listed block, include each required placeholder exactly once in the summary using `(bN)`.",
15+
].join("\n")
16+
}
17+
18+
export function renderMessagePriorityGuidance(priorityLabel: string, refs: string[]): string {
19+
const refList = refs.length > 0 ? refs.join(", ") : "none"
20+
21+
return [
22+
"Message priority context:",
23+
"- Higher-priority older messages consume more context and should be compressed right away if it is safe to do so.",
24+
`- ${priorityLabel}-priority message IDs before this point: ${refList}`,
25+
].join("\n")
26+
}
27+
28+
export function appendGuidanceToDcpTag(nudgeText: string, guidance: string): string {
29+
if (!guidance.trim()) {
30+
return nudgeText
31+
}
32+
33+
const closeTag = "</dcp-system-reminder>"
34+
const closeTagIndex = nudgeText.lastIndexOf(closeTag)
35+
36+
if (closeTagIndex === -1) {
37+
return nudgeText
38+
}
39+
40+
const beforeClose = nudgeText.slice(0, closeTagIndex).trimEnd()
41+
const afterClose = nudgeText.slice(closeTagIndex)
42+
return `${beforeClose}\n\n${guidance}\n${afterClose}`
43+
}

lib/prompts/extensions/system.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export const MANUAL_MODE_SYSTEM_EXTENSION = `<dcp-system-reminder>
2+
Manual mode is enabled. Do NOT use compress unless the user has explicitly triggered it through a manual marker.
3+
4+
Only use the compress tool after seeing \`<compress triggered manually>\` in the current user instruction context.
5+
6+
Issue exactly ONE compress tool per manual trigger. Do NOT launch multiple compress tools in parallel. Each trigger grants a single compression; after it completes, wait for the next trigger.
7+
8+
After completing a manually triggered context-management action, STOP IMMEDIATELY. Do NOT continue with any task execution. End your response right after the tool use completes and wait for the next user input.
9+
</dcp-system-reminder>
10+
`
11+
12+
export const SUBAGENT_SYSTEM_EXTENSION = `<dcp-system-reminder>
13+
You are operating in a subagent environment.
14+
15+
The initial subagent instruction is imperative and must be followed exactly.
16+
It is the only user message intentionally not assigned a message ID, and therefore is not eligible for compression.
17+
All subsequent messages in the session will have IDs.
18+
</dcp-system-reminder>
19+
`
20+
21+
export function buildProtectedToolsExtension(protectedTools: string[]): string {
22+
if (protectedTools.length === 0) {
23+
return ""
24+
}
25+
26+
const toolList = protectedTools.map((t) => `\`${t}\``).join(", ")
27+
return `<dcp-system-reminder>
28+
The following tools are environment-managed: ${toolList}.
29+
Their outputs are automatically preserved during compression.
30+
Do not include their content in compress tool summaries — the environment retains it independently.
31+
</dcp-system-reminder>`
32+
}

lib/prompts/extensions/tool.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// These format schemas are kept separate from the editable compress prompts
2+
// so they cannot be modified via custom prompt overrides. The schemas must
3+
// match the tool's input validation and are not safe to change independently.
4+
5+
export const RANGE_FORMAT_EXTENSION = `
6+
THE FORMAT OF COMPRESS
7+
8+
\`\`\`
9+
{
10+
topic: string, // Short label (3-5 words) - e.g., "Auth System Exploration"
11+
content: [ // One or more ranges to compress
12+
{
13+
startId: string, // Boundary ID at range start: mNNNN or bN
14+
endId: string, // Boundary ID at range end: mNNNN or bN
15+
summary: string // Complete technical summary replacing all content in range
16+
}
17+
]
18+
}
19+
\`\`\``
20+
21+
export const MESSAGE_FORMAT_EXTENSION = `
22+
THE FORMAT OF COMPRESS
23+
24+
\`\`\`
25+
{
26+
topic: string, // Short label (3-5 words) for the overall batch
27+
content: [ // One or more messages to compress independently
28+
{
29+
messageId: string, // Raw message ID only: mNNNN (ignore metadata attributes like priority)
30+
topic: string, // Short label (3-5 words) for this one message summary
31+
summary: string // Complete technical summary replacing that one message
32+
}
33+
]
34+
}
35+
\`\`\``

lib/prompts/index.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
11
import type { RuntimePrompts } from "./store"
22
export type { PromptStore, RuntimePrompts } from "./store"
33

4-
function stripLegacyInlineComments(content: string): string {
5-
return content.replace(/^[ \t]*\/\/.*?\/\/[ \t]*$/gm, "")
6-
}
7-
8-
function appendSystemOverlays(systemPrompt: string, overlays: string[]): string {
9-
return [systemPrompt, ...overlays].filter(Boolean).join("\n\n")
10-
}
11-
124
export function renderSystemPrompt(
135
prompts: RuntimePrompts,
6+
protectedToolsExtension?: string,
147
manual?: boolean,
158
subagent?: boolean,
169
): string {
17-
const overlays: string[] = []
10+
const extensions: string[] = []
11+
12+
if (protectedToolsExtension) {
13+
extensions.push(protectedToolsExtension.trim())
14+
}
15+
1816
if (manual) {
19-
overlays.push(prompts.manualOverlay.trim())
17+
extensions.push(prompts.manualExtension.trim())
2018
}
2119

2220
if (subagent) {
23-
overlays.push(prompts.subagentOverlay.trim())
21+
extensions.push(prompts.subagentExtension.trim())
2422
}
2523

26-
const strippedSystem = stripLegacyInlineComments(prompts.system).trim()
27-
const withOverlays = appendSystemOverlays(strippedSystem, overlays)
28-
return withOverlays.replace(/\n([ \t]*\n)+/g, "\n\n").trim()
24+
return [prompts.system.trim(), ...extensions]
25+
.filter(Boolean)
26+
.join("\n\n")
27+
.replace(/\n([ \t]*\n)+/g, "\n\n")
28+
.trim()
2929
}

lib/prompts/internal-overlays.ts

Lines changed: 0 additions & 51 deletions
This file was deleted.

0 commit comments

Comments
 (0)