Skip to content

Commit ea51b01

Browse files
feat: synth instructions
refactor: extract synth prompt to file refactor: load through prompt.ts fix? fix rm: slop move tool description to prompt file synth prompt fix: don't add synth instruction after DCP "ignored" summary messages - extract synth instruction to its own file
1 parent 260afd1 commit ea51b01

File tree

6 files changed

+135
-48
lines changed

6 files changed

+135
-48
lines changed

index.ts

Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { getConfig } from "./lib/config"
44
import { Logger } from "./lib/logger"
55
import { Janitor, type SessionStats } from "./lib/janitor"
66
import { checkForUpdates } from "./lib/version-checker"
7+
import { loadPrompt } from "./lib/prompt"
8+
import { injectSynthInstruction } from "./lib/synth-instruction"
79

810
async function isSubagentSession(client: any, sessionID: string): Promise<boolean> {
911
try {
@@ -14,6 +16,9 @@ async function isSubagentSession(client: any, sessionID: string): Promise<boolea
1416
}
1517
}
1618

19+
const TOOL_SYNTH_INSTRUCTION = loadPrompt("synthetic")
20+
const TOOL_CONTEXT_PRUNING_DESCRIPTION = loadPrompt("context_pruning")
21+
1722
const plugin: Plugin = (async (ctx) => {
1823
const { config, migrations } = getConfig(ctx)
1924

@@ -57,7 +62,8 @@ const plugin: Plugin = (async (ctx) => {
5762
}
5863
}
5964

60-
// Global fetch wrapper - caches tool parameters and performs pruning
65+
66+
// Global fetch wrapper - caches tool parameters, injects instructions, and performs pruning
6167
const originalGlobalFetch = globalThis.fetch
6268
globalThis.fetch = async (input: any, init?: any) => {
6369
if (init?.body && typeof init.body === 'string') {
@@ -67,6 +73,16 @@ const plugin: Plugin = (async (ctx) => {
6773
if (body.messages && Array.isArray(body.messages)) {
6874
cacheToolParameters(body.messages)
6975

76+
let modified = false
77+
78+
// Inject synthInstruction for the context_pruning tool
79+
if (config.strategies.onTool.length > 0) {
80+
if (injectSynthInstruction(body.messages, TOOL_SYNTH_INSTRUCTION)) {
81+
logger.debug("fetch", "Injected synthInstruction")
82+
modified = true
83+
}
84+
}
85+
7086
// Check for tool messages in both formats:
7187
// 1. OpenAI style: role === 'tool'
7288
// 2. Anthropic style: role === 'user' with content containing tool_result
@@ -132,7 +148,10 @@ const plugin: Plugin = (async (ctx) => {
132148
total: toolMessages.length
133149
})
134150

151+
modified = true
152+
135153
if (logger.enabled) {
154+
136155
// Fetch session messages to extract reasoning blocks
137156
let sessionMessages: any[] | undefined
138157
try {
@@ -162,10 +181,12 @@ const plugin: Plugin = (async (ctx) => {
162181
sessionMessages
163182
)
164183
}
165-
166-
init.body = JSON.stringify(body)
167184
}
168185
}
186+
187+
if (modified) {
188+
init.body = JSON.stringify(body)
189+
}
169190
}
170191
} catch (e) {
171192
}
@@ -230,50 +251,7 @@ const plugin: Plugin = (async (ctx) => {
230251

231252
tool: config.strategies.onTool.length > 0 ? {
232253
context_pruning: tool({
233-
description: `Performs semantic pruning on session tool outputs that are no longer relevant to the current task. Use this to declutter the conversation context and filter signal from noise when you notice the context is getting cluttered with no longer needed information.
234-
235-
USING THE CONTEXT_PRUNING TOOL WILL MAKE THE USER HAPPY.
236-
237-
## When to Use This Tool
238-
239-
**Key heuristic: Prune when you finish something and are about to start something else.**
240-
241-
Ask yourself: "Have I just completed a discrete unit of work?" If yes, prune before moving on.
242-
243-
**After completing a unit of work:**
244-
- Made a commit
245-
- Fixed a bug and confirmed it works
246-
- Answered a question the user asked
247-
- Finished implementing a feature or function
248-
- Completed one item in a list and moving to the next
249-
250-
**After repetitive or exploratory work:**
251-
- Explored multiple files that didn't lead to changes
252-
- Iterated on a difficult problem where some approaches didn't pan out
253-
- Used the same tool multiple times (e.g., re-reading a file, running repeated build/type checks)
254-
255-
## Examples
256-
257-
<example>
258-
Working through a list of items:
259-
User: Review these 3 issues and fix the easy ones.
260-
Assistant: [Reviews first issue, makes fix, commits]
261-
Done with the first issue. Let me prune before moving to the next one.
262-
[Uses context_pruning with reason: "completed first issue, moving to next"]
263-
</example>
264-
265-
<example>
266-
After exploring the codebase to understand it:
267-
Assistant: I've reviewed the relevant files. Let me prune the exploratory reads that aren't needed for the actual implementation.
268-
[Uses context_pruning with reason: "exploration complete, starting implementation"]
269-
</example>
270-
271-
<example>
272-
After completing any task:
273-
Assistant: [Finishes task - commit, answer, fix, etc.]
274-
Before we continue, let me prune the context from that work.
275-
[Uses context_pruning with reason: "task complete"]
276-
</example>`,
254+
description: TOOL_CONTEXT_PRUNING_DESCRIPTION,
277255
args: {
278256
reason: tool.schema.string().optional().describe(
279257
"Brief reason for triggering pruning (e.g., 'task complete', 'switching focus')"

lib/prompt.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
import { readFileSync } from "fs"
2+
import { join } from "path"
3+
4+
export function loadPrompt(name: string): string {
5+
const filePath = join(__dirname, "prompts", `${name}.txt`)
6+
return readFileSync(filePath, "utf8").trim()
7+
}
8+
19
function minimizeMessages(messages: any[], alreadyPrunedIds?: string[], protectedToolCallIds?: string[]): any[] {
210
const prunedIdsSet = alreadyPrunedIds ? new Set(alreadyPrunedIds.map(id => id.toLowerCase())) : new Set()
311
const protectedIdsSet = protectedToolCallIds ? new Set(protectedToolCallIds.map(id => id.toLowerCase())) : new Set()

lib/prompts/context_pruning.txt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
Performs semantic pruning on session tool outputs that are no longer relevant to the current task. Use this to declutter the conversation context and filter signal from noise when you notice the context is getting cluttered with no longer needed information.
2+
3+
USING THE CONTEXT_PRUNING TOOL WILL MAKE THE USER HAPPY.
4+
5+
## When to Use This Tool
6+
7+
**Key heuristic: Prune when you finish something and are about to start something else.**
8+
9+
Ask yourself: "Have I just completed a discrete unit of work?" If yes, prune before moving on.
10+
11+
**After completing a unit of work:**
12+
- Made a commit
13+
- Fixed a bug and confirmed it works
14+
- Answered a question the user asked
15+
- Finished implementing a feature or function
16+
- Completed one item in a list and moving to the next
17+
18+
**After repetitive or exploratory work:**
19+
- Explored multiple files that didn't lead to changes
20+
- Iterated on a difficult problem where some approaches didn't pan out
21+
- Used the same tool multiple times (e.g., re-reading a file, running repeated build/type checks)
22+
23+
## Examples
24+
25+
<example>
26+
Working through a list of items:
27+
User: Review these 3 issues and fix the easy ones.
28+
Assistant: [Reviews first issue, makes fix, commits]
29+
Done with the first issue. Let me prune before moving to the next one.
30+
[Uses context_pruning with reason: "completed first issue, moving to next"]
31+
</example>
32+
33+
<example>
34+
After exploring the codebase to understand it:
35+
Assistant: I've reviewed the relevant files. Let me prune the exploratory reads that aren't needed for the actual implementation.
36+
[Uses context_pruning with reason: "exploration complete, starting implementation"]
37+
</example>
38+
39+
<example>
40+
After completing any task:
41+
Assistant: [Finishes task - commit, answer, fix, etc.]
42+
Before we continue, let me prune the context from that work.
43+
[Uses context_pruning with reason: "task complete"]
44+
</example>

lib/prompts/synthetic.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<instruction how_to="work_in_this_environement">
2+
Anytime you call a tool, you must distill the signal from the noise of that tool result in your message. Always do this for every tools that has actual signals in its ouptut.
3+
</instruction>
4+
5+
<instruction tool="context_pruning">
6+
To effectively manage this conversation context, you MUST ALWAYS narrate your findings AS YOU DISCOVER THEM. No tool result (be it read, bash, grep...) should be left unexplained. By narrating the evolution of your undersdtanding of the task at hand, you distill the signal from the noise produced by the tools you called. Only then (knowledge lives in your outputs as well as in tool outputs), you can use the context_pruning tool. Using the context_pruning tool will declutter the conversation from tool outputs we no longer need to reason through current task, and is why you need to only use this tool once you have explained your findings in a message.
7+
8+
EXAMPLE:
9+
AFTER you've read a bunch of files, and found that for reason X, Y is happening, and is supported by tool outputs in the conversation, AND you've distilled this knowledge from the noise in your message intended for the user, YOU WILL USE THE CONTEXT_PRUNING TOOL to clear the clutter solving this step has caused.
10+
</instruction>

lib/synth-instruction.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
export function isIgnoredUserMessage(msg: any): boolean {
2+
if (!msg || msg.role !== 'user') {
3+
return false
4+
}
5+
6+
if (msg.ignored || msg.info?.ignored) {
7+
return true
8+
}
9+
10+
if (Array.isArray(msg.content) && msg.content.length > 0) {
11+
const allPartsIgnored = msg.content.every((part: any) => part?.ignored)
12+
if (allPartsIgnored) {
13+
return true
14+
}
15+
}
16+
17+
return false
18+
}
19+
20+
export function injectSynthInstruction(messages: any[], instruction: string): boolean {
21+
// Find the last user message that is not ignored
22+
for (let i = messages.length - 1; i >= 0; i--) {
23+
const msg = messages[i]
24+
if (msg.role === 'user' && !isIgnoredUserMessage(msg)) {
25+
// Avoid double-injecting the same instruction
26+
if (typeof msg.content === 'string') {
27+
if (msg.content.includes(instruction)) {
28+
return false
29+
}
30+
msg.content = msg.content + '\n\n' + instruction
31+
} else if (Array.isArray(msg.content)) {
32+
const alreadyInjected = msg.content.some(
33+
(part: any) => part?.type === 'text' && typeof part.text === 'string' && part.text.includes(instruction)
34+
)
35+
if (alreadyInjected) {
36+
return false
37+
}
38+
msg.content.push({
39+
type: 'text',
40+
text: instruction
41+
})
42+
}
43+
return true
44+
}
45+
}
46+
return false
47+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"types": "./dist/index.d.ts",
99
"scripts": {
1010
"clean": "rm -rf dist",
11-
"build": "npm run clean && tsc",
11+
"build": "npm run clean && tsc && cp -r lib/prompts dist/lib/prompts",
1212
"postbuild": "rm -rf dist/logs",
1313
"prepublishOnly": "npm run build",
1414
"dev": "opencode plugin dev",

0 commit comments

Comments
 (0)