@@ -2,34 +2,45 @@ import type { SessionState, WithParts } from "../state"
22import type { Logger } from "../logger"
33import type { PluginConfig } from "../config"
44import { loadPrompt } from "../prompt"
5- import { extractParameterKey , buildToolIdList } from "./utils"
5+ import {
6+ extractParameterKey ,
7+ buildToolIdList ,
8+ createSyntheticUserMessage ,
9+ createSyntheticAssistantMessage ,
10+ } from "./utils"
611import { getLastAssistantMessage , getLastUserMessage , isMessageCompacted } from "../shared-utils"
7- import { AssistantMessage , UserMessage } from "@opencode-ai/sdk"
812
913const PRUNED_TOOL_INPUT_REPLACEMENT =
1014 "[content removed to save context, this is not what was written to the file, but a placeholder]"
1115const PRUNED_TOOL_OUTPUT_REPLACEMENT =
1216 "[Output removed to save context - information superseded or no longer needed]"
13- const getNudgeString = ( config : PluginConfig ) : string => {
17+
18+ const getNudgeString = ( config : PluginConfig , isReasoningModel : boolean ) : string => {
1419 const discardEnabled = config . tools . discard . enabled
1520 const extractEnabled = config . tools . extract . enabled
21+ const roleDir = isReasoningModel ? "user" : "assistant"
1622
1723 if ( discardEnabled && extractEnabled ) {
18- return loadPrompt ( " nudge/nudge-both" )
24+ return loadPrompt ( ` ${ roleDir } / nudge/nudge-both` )
1925 } else if ( discardEnabled ) {
20- return loadPrompt ( " nudge/nudge-discard" )
26+ return loadPrompt ( ` ${ roleDir } / nudge/nudge-discard` )
2127 } else if ( extractEnabled ) {
22- return loadPrompt ( " nudge/nudge-extract" )
28+ return loadPrompt ( ` ${ roleDir } / nudge/nudge-extract` )
2329 }
2430 return ""
2531}
2632
27- const wrapPrunableTools = ( content : string ) : string => `<prunable-tools>
33+ const wrapPrunableToolsUser = ( content : string ) : string => `<prunable-tools>
34+ The following tools have been invoked and are available for pruning. This list does not mandate immediate action. Consider your current goals and the resources you need before discarding valuable tool inputs or outputs. Consolidate your prunes for efficiency; it is rarely worth pruning a single tiny tool output. Keep the context free of noise.
35+ ${ content }
36+ </prunable-tools>`
37+
38+ const wrapPrunableToolsAssistant = ( content : string ) : string => `<prunable-tools>
2839I have the following tool outputs available for pruning. I should consider my current goals and the resources I need before discarding valuable inputs or outputs. I should consolidate prunes for efficiency; it is rarely worth pruning a single tiny tool output.
2940${ content }
3041</prunable-tools>`
3142
32- const getCooldownMessage = ( config : PluginConfig ) : string => {
43+ const getCooldownMessage = ( config : PluginConfig , isReasoningModel : boolean ) : string => {
3344 const discardEnabled = config . tools . discard . enabled
3445 const extractEnabled = config . tools . extract . enabled
3546
@@ -42,16 +53,12 @@ const getCooldownMessage = (config: PluginConfig): string => {
4253 toolName = "extract tool"
4354 }
4455
45- return `<prunable-tools>
46- I just performed context management. I will not use the ${ toolName } again until after my next tool use, when a fresh list will be available.
47- </prunable-tools>`
48- }
56+ const message = isReasoningModel
57+ ? `Context management was just performed. Do not use the ${ toolName } again. A fresh list will be available after your next tool use.`
58+ : `I just performed context management. I will not use the ${ toolName } again until after my next tool use, when a fresh list will be available.`
4959
50- const SYNTHETIC_MESSAGE_ID = "msg_01234567890123456789012345"
51- const SYNTHETIC_PART_ID = "prt_01234567890123456789012345"
52- const SYNTHETIC_USER_MESSAGE_ID = "msg_01234567890123456789012346"
53- const SYNTHETIC_USER_PART_ID = "prt_01234567890123456789012346"
54- const REASONING_MODEL_USER_MESSAGE_CONTENT = "[internal: context sync - no response needed]"
60+ return `<prunable-tools>\n${ message } \n</prunable-tools>`
61+ }
5562
5663const buildPrunableToolsList = (
5764 state : SessionState ,
@@ -92,7 +99,8 @@ const buildPrunableToolsList = (
9299 return ""
93100 }
94101
95- return wrapPrunableTools ( lines . join ( "\n" ) )
102+ const wrapFn = state . isReasoningModel ? wrapPrunableToolsUser : wrapPrunableToolsAssistant
103+ return wrapFn ( lines . join ( "\n" ) )
96104}
97105
98106export const insertPruneToolContext = (
@@ -105,16 +113,14 @@ export const insertPruneToolContext = (
105113 return
106114 }
107115
108- const lastAssistantMessage = getLastAssistantMessage ( messages )
109- if ( ! lastAssistantMessage ) {
110- return
111- }
116+ // For reasoning models, inject into user role; for non-reasoning, inject into assistant role
117+ const isReasoningModel = state . isReasoningModel
112118
113119 let prunableToolsContent : string
114120
115121 if ( state . lastToolPrune ) {
116122 logger . debug ( "Last tool was prune - injecting cooldown message" )
117- prunableToolsContent = getCooldownMessage ( config )
123+ prunableToolsContent = getCooldownMessage ( config , isReasoningModel )
118124 } else {
119125 const prunableToolsList = buildPrunableToolsList ( state , config , logger , messages )
120126 if ( ! prunableToolsList ) {
@@ -129,69 +135,24 @@ export const insertPruneToolContext = (
129135 state . nudgeCounter >= config . tools . settings . nudgeFrequency
130136 ) {
131137 logger . info ( "Inserting prune nudge message" )
132- nudgeString = "\n" + getNudgeString ( config )
138+ nudgeString = "\n" + getNudgeString ( config , isReasoningModel )
133139 }
134140
135141 prunableToolsContent = prunableToolsList + nudgeString
136142 }
137143
138- const assistantInfo = lastAssistantMessage . info as AssistantMessage
139- const assistantMessage : WithParts = {
140- info : {
141- id : SYNTHETIC_MESSAGE_ID ,
142- sessionID : assistantInfo . sessionID ,
143- role : "assistant" ,
144- parentID : assistantInfo . parentID ,
145- modelID : assistantInfo . modelID ,
146- providerID : assistantInfo . providerID ,
147- time : { created : Date . now ( ) } ,
148- tokens : { input : 0 , output : 0 , reasoning : 0 , cache : { read : 0 , write : 0 } } ,
149- cost : 0 ,
150- path : assistantInfo . path ,
151- mode : assistantInfo . mode ,
152- } ,
153- parts : [
154- {
155- id : SYNTHETIC_PART_ID ,
156- sessionID : assistantInfo . sessionID ,
157- messageID : SYNTHETIC_MESSAGE_ID ,
158- type : "text" ,
159- text : prunableToolsContent ,
160- } ,
161- ] ,
162- }
163-
164- messages . push ( assistantMessage )
165-
166- // For reasoning models, append a synthetic user message to close the assistant turn.
167- if ( state . isReasoningModel ) {
168- const lastRealUserMessage = getLastUserMessage ( messages )
169- const userMessageInfo = lastRealUserMessage ?. info as UserMessage | undefined
170-
171- const userMessage : WithParts = {
172- info : {
173- id : SYNTHETIC_USER_MESSAGE_ID ,
174- sessionID : assistantInfo . sessionID ,
175- role : "user" ,
176- time : { created : Date . now ( ) + 1 } ,
177- agent : userMessageInfo ?. agent ?? "code" ,
178- model : userMessageInfo ?. model ?? {
179- providerID : assistantInfo . providerID ,
180- modelID : assistantInfo . modelID ,
181- } ,
182- } as UserMessage ,
183- parts : [
184- {
185- id : SYNTHETIC_USER_PART_ID ,
186- sessionID : assistantInfo . sessionID ,
187- messageID : SYNTHETIC_USER_MESSAGE_ID ,
188- type : "text" ,
189- text : REASONING_MODEL_USER_MESSAGE_CONTENT ,
190- } ,
191- ] ,
144+ if ( isReasoningModel ) {
145+ const lastUserMessage = getLastUserMessage ( messages )
146+ if ( ! lastUserMessage ) {
147+ return
148+ }
149+ messages . push ( createSyntheticUserMessage ( lastUserMessage , prunableToolsContent ) )
150+ } else {
151+ const lastAssistantMessage = getLastAssistantMessage ( messages )
152+ if ( ! lastAssistantMessage ) {
153+ return
192154 }
193- messages . push ( userMessage )
194- logger . debug ( "Appended synthetic user message for reasoning model" )
155+ messages . push ( createSyntheticAssistantMessage ( lastAssistantMessage , prunableToolsContent ) )
195156 }
196157}
197158
@@ -218,7 +179,6 @@ const pruneToolOutputs = (state: SessionState, logger: Logger, messages: WithPar
218179 if ( ! state . prune . toolIds . includes ( part . callID ) ) {
219180 continue
220181 }
221- // Skip write and edit tools - their inputs are pruned instead
222182 if ( part . tool === "write" || part . tool === "edit" ) {
223183 continue
224184 }
@@ -238,16 +198,13 @@ const pruneToolInputs = (state: SessionState, logger: Logger, messages: WithPart
238198 if ( ! state . prune . toolIds . includes ( part . callID ) ) {
239199 continue
240200 }
241- // Only prune inputs for write and edit tools
242201 if ( part . tool !== "write" && part . tool !== "edit" ) {
243202 continue
244203 }
245- // Don't prune yet if tool is still pending or running
246204 if ( part . state . status === "pending" || part . state . status === "running" ) {
247205 continue
248206 }
249207
250- // Write tool has content field, edit tool has oldString/newString fields
251208 if ( part . tool === "write" && part . state . input ?. content !== undefined ) {
252209 part . state . input . content = PRUNED_TOOL_INPUT_REPLACEMENT
253210 }
0 commit comments