Skip to content

Commit 91e6dfd

Browse files
committed
Prevent double-sending of thinking content in ACP
Track whether a model sends proper ReasoningDeltaEvent events. If so, skip parsing <thinking> tags from text to avoid sending reasoning content twice (once as proper reasoning, once parsed from text). Also reset the tracking state at the start of each new prompt turn.
1 parent b6a0c4b commit 91e6dfd

1 file changed

Lines changed: 35 additions & 16 deletions

File tree

internal/acpserver/agent.go

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ type Agent struct {
4343
// inThinkingTag tracks whether we're currently inside a <thinking> tag
4444
// when parsing streaming content from models that wrap reasoning in XML tags.
4545
inThinkingTag bool
46+
47+
// hasProperReasoningEvents tracks whether the model is sending ReasoningDeltaEvent
48+
// (proper reasoning events) vs wrapping reasoning in <thinking> tags in text.
49+
// If true, we skip thinking tag parsing to avoid double-sending reasoning.
50+
hasProperReasoningEvents bool
4651
}
4752

4853
// NewAgent creates a new ACP agent backed by Kit.
@@ -141,6 +146,10 @@ func (a *Agent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.Promp
141146

142147
log.Debug("acp: prompt", "session", sessionID, "prompt_len", len(promptText), "files", len(files))
143148

149+
// Reset reasoning tracking for this new prompt turn
150+
a.hasProperReasoningEvents = false
151+
a.inThinkingTag = false
152+
144153
// Create a cancellable context for this prompt turn.
145154
promptCtx, cancel := context.WithCancel(ctx)
146155
sess.setCancel(cancel)
@@ -206,26 +215,36 @@ func (a *Agent) subscribeEvents(ctx context.Context, k *kit.Kit, sessionID acp.S
206215
var update *acp.SessionUpdate
207216
switch ev := e.(type) {
208217
case kit.MessageUpdateEvent:
209-
// Handle models that wrap reasoning in <thinking> tags (Qwen, DeepSeek)
210-
// Parse the chunk and separate reasoning from regular text
211-
reasoning, text := a.parseThinkingTags(ev.Chunk)
212-
213-
// Send reasoning update if we have reasoning content
214-
if reasoning != "" {
215-
u := acp.UpdateAgentThoughtText(reasoning)
216-
_ = a.conn.SessionUpdate(ctx, acp.SessionNotification{
217-
SessionId: sessionID,
218-
Update: u,
219-
})
220-
}
221-
222-
// Send text update if we have text content
223-
if text != "" {
224-
u := acp.UpdateAgentMessageText(text)
218+
// If the model sends proper ReasoningDeltaEvent, don't parse thinking tags
219+
// from text to avoid double-sending reasoning content.
220+
if a.hasProperReasoningEvents {
221+
// Send text as-is without thinking tag parsing
222+
u := acp.UpdateAgentMessageText(ev.Chunk)
225223
update = &u
224+
} else {
225+
// Handle models that wrap reasoning in <thinking> tags (Qwen, DeepSeek)
226+
// Parse the chunk and separate reasoning from regular text
227+
reasoning, text := a.parseThinkingTags(ev.Chunk)
228+
229+
// Send reasoning update if we have reasoning content
230+
if reasoning != "" {
231+
u := acp.UpdateAgentThoughtText(reasoning)
232+
_ = a.conn.SessionUpdate(ctx, acp.SessionNotification{
233+
SessionId: sessionID,
234+
Update: u,
235+
})
236+
}
237+
238+
// Send text update if we have text content
239+
if text != "" {
240+
u := acp.UpdateAgentMessageText(text)
241+
update = &u
242+
}
226243
}
227244

228245
case kit.ReasoningDeltaEvent:
246+
// Track that this model sends proper reasoning events
247+
a.hasProperReasoningEvents = true
229248
u := acp.UpdateAgentThoughtText(ev.Delta)
230249
update = &u
231250

0 commit comments

Comments
 (0)