Skip to content

Commit 065b51a

Browse files
authored
Merge pull request #45 from Tarquinen/fix/anthropic-nudge-compatibility
fix: remove synthetic field from nudge messages for Anthropic compatibility
2 parents 5227432 + 1c51730 commit 065b51a

File tree

9 files changed

+60
-37
lines changed

9 files changed

+60
-37
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Add to your OpenCode config:
1313
```jsonc
1414
// opencode.jsonc
1515
{
16-
"plugin": ["@tarquinen/[email protected].26"]
16+
"plugin": ["@tarquinen/[email protected].27"]
1717
}
1818
```
1919

lib/fetch-wrapper/gemini.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export async function handleGemini(
3737
ctx.logger.debug("fetch", "skipNextIdle was reset by new tool results (Gemini)")
3838
}
3939

40-
if (injectSynthGemini(body.contents, ctx.prompts.synthInstruction)) {
40+
if (injectSynthGemini(body.contents, ctx.prompts.synthInstruction, ctx.prompts.nudgeInstruction)) {
4141
ctx.logger.info("fetch", "Injected synthetic instruction (Gemini)")
4242
modified = true
4343
}

lib/fetch-wrapper/openai-chat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export async function handleOpenAIChatAndAnthropic(
4242
ctx.logger.debug("fetch", "skipNextIdle was reset by new tool results")
4343
}
4444

45-
if (injectSynth(body.messages, ctx.prompts.synthInstruction)) {
45+
if (injectSynth(body.messages, ctx.prompts.synthInstruction, ctx.prompts.nudgeInstruction)) {
4646
ctx.logger.info("fetch", "Injected synthetic instruction")
4747
modified = true
4848
}

lib/fetch-wrapper/openai-responses.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export async function handleOpenAIResponses(
4242
ctx.logger.debug("fetch", "skipNextIdle was reset by new tool results (Responses API)")
4343
}
4444

45-
if (injectSynthResponses(body.input, ctx.prompts.synthInstruction)) {
45+
if (injectSynthResponses(body.input, ctx.prompts.synthInstruction, ctx.prompts.nudgeInstruction)) {
4646
ctx.logger.info("fetch", "Injected synthetic instruction (Responses API)")
4747
modified = true
4848
}

lib/hooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function createEventHandler(
4545
// Reset nudge counter if idle pruning succeeded and covers tool strategies
4646
if (result && result.prunedCount > 0 && toolTracker && config.nudge_freq > 0) {
4747
if (toolStrategiesCoveredByIdle(config.strategies.onIdle, config.strategies.onTool)) {
48-
resetToolTrackerCount(toolTracker, config.nudge_freq)
48+
resetToolTrackerCount(toolTracker)
4949
}
5050
}
5151
} catch (err: any) {

lib/pruning-tool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export function createPruningTool(janitor: Janitor, config: PluginConfig, toolTr
3232

3333
// Reset nudge counter to prevent immediate re-nudging after pruning
3434
if (config.nudge_freq > 0) {
35-
resetToolTrackerCount(toolTracker, config.nudge_freq)
35+
resetToolTrackerCount(toolTracker)
3636
}
3737

3838
const postPruneGuidance = "\n\nYou have already distilled relevant understanding in writing before calling this tool. Do not re-narrate; continue with your next task."

lib/synth-instruction.ts

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export interface ToolTracker {
22
seenToolResultIds: Set<string>
3-
toolResultCount: number
3+
toolResultCount: number // Tools since last prune
44
skipNextIdle: boolean
55
getToolName?: (callId: string) => string | undefined
66
}
@@ -9,9 +9,9 @@ export function createToolTracker(): ToolTracker {
99
return { seenToolResultIds: new Set(), toolResultCount: 0, skipNextIdle: false }
1010
}
1111

12-
export function resetToolTrackerCount(tracker: ToolTracker, freq: number): void {
13-
const currentBucket = Math.floor(tracker.toolResultCount / freq)
14-
tracker.toolResultCount = currentBucket * freq
12+
/** Reset tool count to 0 (called after a prune event) */
13+
export function resetToolTrackerCount(tracker: ToolTracker): void {
14+
tracker.toolResultCount = 0
1515
}
1616

1717
/** Adapter interface for format-specific message operations */
@@ -20,23 +20,21 @@ interface MessageFormatAdapter {
2020
appendNudge(messages: any[], nudgeText: string): void
2121
}
2222

23-
/** Generic nudge injection - counts tool results and injects nudge every N results */
23+
/** Generic nudge injection - nudges every fetch once tools since last prune exceeds freq */
2424
function injectNudgeCore(
2525
messages: any[],
2626
tracker: ToolTracker,
2727
nudgeText: string,
2828
freq: number,
2929
adapter: MessageFormatAdapter
3030
): boolean {
31-
const prevCount = tracker.toolResultCount
32-
const newCount = adapter.countToolResults(messages, tracker)
33-
if (newCount > 0) {
34-
const prevBucket = Math.floor(prevCount / freq)
35-
const newBucket = Math.floor(tracker.toolResultCount / freq)
36-
if (newBucket > prevBucket) {
37-
adapter.appendNudge(messages, nudgeText)
38-
return true
39-
}
31+
// Count any new tool results
32+
adapter.countToolResults(messages, tracker)
33+
34+
// Once we've exceeded the threshold, nudge on every fetch
35+
if (tracker.toolResultCount > freq) {
36+
adapter.appendNudge(messages, nudgeText)
37+
return true
4038
}
4139
return false
4240
}
@@ -79,27 +77,29 @@ const openaiAdapter: MessageFormatAdapter = {
7977
return newCount
8078
},
8179
appendNudge(messages, nudgeText) {
82-
messages.push({ role: 'user', content: nudgeText, synthetic: true })
80+
messages.push({ role: 'user', content: nudgeText })
8381
}
8482
}
8583

86-
export function isIgnoredUserMessage(msg: any): boolean {
87-
if (!msg || msg.role !== 'user') return false
88-
if (msg.ignored || msg.info?.ignored || msg.synthetic) return true
89-
if (Array.isArray(msg.content) && msg.content.length > 0) {
90-
if (msg.content.every((part: any) => part?.ignored)) return true
91-
}
92-
return false
93-
}
94-
9584
export function injectNudge(messages: any[], tracker: ToolTracker, nudgeText: string, freq: number): boolean {
9685
return injectNudgeCore(messages, tracker, nudgeText, freq, openaiAdapter)
9786
}
9887

99-
export function injectSynth(messages: any[], instruction: string): boolean {
88+
/** Check if a message content matches nudge text (OpenAI/Anthropic format) */
89+
function isNudgeMessage(msg: any, nudgeText: string): boolean {
90+
if (typeof msg.content === 'string') {
91+
return msg.content === nudgeText
92+
}
93+
return false
94+
}
95+
96+
export function injectSynth(messages: any[], instruction: string, nudgeText: string): boolean {
10097
for (let i = messages.length - 1; i >= 0; i--) {
10198
const msg = messages[i]
102-
if (msg.role === 'user' && !isIgnoredUserMessage(msg)) {
99+
if (msg.role === 'user') {
100+
// Skip nudge messages - find real user message
101+
if (isNudgeMessage(msg, nudgeText)) continue
102+
103103
if (typeof msg.content === 'string') {
104104
if (msg.content.includes(instruction)) return false
105105
msg.content = msg.content + '\n\n' + instruction
@@ -151,10 +151,22 @@ export function injectNudgeGemini(contents: any[], tracker: ToolTracker, nudgeTe
151151
return injectNudgeCore(contents, tracker, nudgeText, freq, geminiAdapter)
152152
}
153153

154-
export function injectSynthGemini(contents: any[], instruction: string): boolean {
154+
/** Check if a Gemini content matches nudge text */
155+
function isNudgeContentGemini(content: any, nudgeText: string): boolean {
156+
if (Array.isArray(content.parts) && content.parts.length === 1) {
157+
const part = content.parts[0]
158+
return part?.text === nudgeText
159+
}
160+
return false
161+
}
162+
163+
export function injectSynthGemini(contents: any[], instruction: string, nudgeText: string): boolean {
155164
for (let i = contents.length - 1; i >= 0; i--) {
156165
const content = contents[i]
157166
if (content.role === 'user' && Array.isArray(content.parts)) {
167+
// Skip nudge messages - find real user message
168+
if (isNudgeContentGemini(content, nudgeText)) continue
169+
158170
const alreadyInjected = content.parts.some(
159171
(part: any) => part?.text && typeof part.text === 'string' && part.text.includes(instruction)
160172
)
@@ -198,10 +210,21 @@ export function injectNudgeResponses(input: any[], tracker: ToolTracker, nudgeTe
198210
return injectNudgeCore(input, tracker, nudgeText, freq, responsesAdapter)
199211
}
200212

201-
export function injectSynthResponses(input: any[], instruction: string): boolean {
213+
/** Check if a Responses API item matches nudge text */
214+
function isNudgeItemResponses(item: any, nudgeText: string): boolean {
215+
if (typeof item.content === 'string') {
216+
return item.content === nudgeText
217+
}
218+
return false
219+
}
220+
221+
export function injectSynthResponses(input: any[], instruction: string, nudgeText: string): boolean {
202222
for (let i = input.length - 1; i >= 0; i--) {
203223
const item = input[i]
204224
if (item.type === 'message' && item.role === 'user') {
225+
// Skip nudge messages - find real user message
226+
if (isNudgeItemResponses(item, nudgeText)) continue
227+
205228
if (typeof item.content === 'string') {
206229
if (item.content.includes(instruction)) return false
207230
item.content = item.content + '\n\n' + instruction

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://json.schemastore.org/package.json",
33
"name": "@tarquinen/opencode-dcp",
4-
"version": "0.3.26",
4+
"version": "0.3.27",
55
"type": "module",
66
"description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context",
77
"main": "./dist/index.js",

0 commit comments

Comments
 (0)