Skip to content

Commit 6f4f56b

Browse files
committed
do not add reasoning tokens to message history
1 parent f692391 commit 6f4f56b

File tree

3 files changed

+69
-37
lines changed

3 files changed

+69
-37
lines changed

backend/src/llm-apis/vercel-ai-sdk/ai-sdk.ts

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,6 @@ import {
55
geminiModels,
66
openaiModels,
77
} from '@codebuff/common/old-constants'
8-
import {
9-
endToolTag,
10-
startToolTag,
11-
toolNameParam,
12-
} from '@codebuff/common/tools/constants'
138
import { buildArray } from '@codebuff/common/util/array'
149
import { convertCbToModelMessages } from '@codebuff/common/util/messages'
1510
import { errorToObject } from '@codebuff/common/util/object'
@@ -36,6 +31,17 @@ import type {
3631
import type { LanguageModel } from 'ai'
3732
import type { z } from 'zod/v4'
3833

34+
export type StreamChunk =
35+
| {
36+
type: 'text'
37+
text: string
38+
}
39+
| {
40+
type: 'reasoning'
41+
text: string
42+
}
43+
| { type: 'error'; message: string }
44+
3945
// TODO: We'll want to add all our models here!
4046
const modelToAiSDKModel = (model: Model): LanguageModel => {
4147
if (
@@ -77,7 +83,7 @@ export const promptAiSdkStream = async function* (
7783
includeCacheControl?: boolean
7884
resolveMessageId?: (messageId: string) => unknown
7985
} & Omit<Parameters<typeof streamText>[0], 'model' | 'messages'>,
80-
) {
86+
): AsyncGenerator<StreamChunk> {
8187
if (
8288
!checkLiveUserInput(
8389
options.userId,
@@ -93,7 +99,10 @@ export const promptAiSdkStream = async function* (
9399
},
94100
'Skipping stream due to canceled user input',
95101
)
96-
yield ''
102+
yield {
103+
type: 'text',
104+
text: '',
105+
}
97106
return
98107
}
99108
const startTime = Date.now()
@@ -108,7 +117,6 @@ export const promptAiSdkStream = async function* (
108117
})
109118

110119
let content = ''
111-
let reasoning = false
112120

113121
for await (const chunk of response.fullStream) {
114122
if (chunk.type === 'error') {
@@ -131,9 +139,11 @@ export const promptAiSdkStream = async function* (
131139
? chunk.error
132140
: JSON.stringify(chunk.error)
133141
const errorMessage = `Error from AI SDK (model ${options.model}): ${buildArray([mainErrorMessage, errorBody]).join('\n')}`
134-
throw new Error(errorMessage, {
135-
cause: chunk.error,
136-
})
142+
yield {
143+
type: 'error',
144+
message: errorMessage,
145+
}
146+
return
137147
}
138148
if (chunk.type === 'reasoning-delta') {
139149
if (
@@ -145,21 +155,17 @@ export const promptAiSdkStream = async function* (
145155
) {
146156
continue
147157
}
148-
if (!reasoning) {
149-
reasoning = true
150-
yield `${startToolTag}{
151-
${JSON.stringify(toolNameParam)}: "think_deeply",
152-
"thought": "`
158+
yield {
159+
type: 'reasoning',
160+
text: chunk.text,
153161
}
154-
yield JSON.stringify(chunk.text).slice(1, -1)
155162
}
156163
if (chunk.type === 'text-delta') {
157-
if (reasoning) {
158-
reasoning = false
159-
yield `"\n}${endToolTag}\n\n`
160-
}
161164
content += chunk.text
162-
yield chunk.text
165+
yield {
166+
type: 'text',
167+
text: chunk.text,
168+
}
163169
}
164170
}
165171

backend/src/tools/stream-parser.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { toolNames } from '@codebuff/common/tools/constants'
1+
import {
2+
endToolTag,
3+
startToolTag,
4+
toolNameParam,
5+
toolNames,
6+
} from '@codebuff/common/tools/constants'
27
import { buildArray } from '@codebuff/common/util/array'
38
import { generateCompactId } from '@codebuff/common/util/string'
49

@@ -8,6 +13,7 @@ import { processStreamWithTags } from '../xml-stream-parser'
813
import { executeCustomToolCall, executeToolCall } from './tool-executor'
914

1015
import type { CustomToolCall } from './tool-executor'
16+
import type { StreamChunk } from '../llm-apis/vercel-ai-sdk/ai-sdk'
1117
import type { AgentTemplate } from '../templates/types'
1218
import type { ToolName } from '@codebuff/common/tools/constants'
1319
import type { CodebuffToolCall } from '@codebuff/common/tools/list'
@@ -25,8 +31,8 @@ export type ToolCallError = {
2531
error: string
2632
} & Omit<ToolCallPart, 'type'>
2733

28-
export async function processStreamWithTools<T extends string>(options: {
29-
stream: AsyncGenerator<T> | ReadableStream<T>
34+
export async function processStreamWithTools(options: {
35+
stream: AsyncGenerator<StreamChunk>
3036
ws: WebSocket
3137
agentStepId: string
3238
clientSessionId: string
@@ -169,9 +175,28 @@ export async function processStreamWithTools<T extends string>(options: {
169175
},
170176
)
171177

178+
let reasoning = false
172179
for await (const chunk of streamWithTags) {
173-
onResponseChunk(chunk)
174-
fullResponseChunks.push(chunk)
180+
if (chunk.type === 'reasoning') {
181+
if (!reasoning) {
182+
reasoning = true
183+
onResponseChunk(`\n\n${startToolTag}{
184+
${JSON.stringify(toolNameParam)}: "think_deeply",
185+
"thought": "`)
186+
}
187+
onResponseChunk(JSON.stringify(chunk.text).slice(1, -1))
188+
} else if (chunk.type === 'text') {
189+
if (reasoning) {
190+
reasoning = false
191+
onResponseChunk(`"\n}${endToolTag}\n\n`)
192+
}
193+
onResponseChunk(chunk.text)
194+
fullResponseChunks.push(chunk.text)
195+
} else if (chunk.type === 'error') {
196+
onResponseChunk(chunk)
197+
} else {
198+
chunk satisfies never
199+
}
175200
}
176201

177202
state.messages = buildArray<Message>([

backend/src/xml-stream-parser.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
toolNameParam,
88
} from '@codebuff/common/tools/constants'
99

10+
import type { StreamChunk } from './llm-apis/vercel-ai-sdk/ai-sdk'
1011
import type { Model } from '@codebuff/common/old-constants'
1112
import type {
1213
PrintModeError,
@@ -22,7 +23,7 @@ const toolExtractionPattern = new RegExp(
2223
const completionSuffix = `${JSON.stringify(endsAgentStepParam)}: true\n}${endToolTag}`
2324

2425
export async function* processStreamWithTags(
25-
stream: AsyncGenerator<string> | ReadableStream<string>,
26+
stream: AsyncGenerator<StreamChunk>,
2627
processors: Record<
2728
string,
2829
{
@@ -39,7 +40,7 @@ export async function* processStreamWithTags(
3940
model?: Model
4041
agentName?: string
4142
},
42-
): AsyncGenerator<string> {
43+
): AsyncGenerator<StreamChunk> {
4344
let streamCompleted = false
4445
let buffer = ''
4546
let autocompleted = false
@@ -131,17 +132,20 @@ export async function* processStreamWithTags(
131132
matches.forEach(processToolCallContents)
132133
}
133134

134-
function* processChunk(chunk: string | undefined) {
135-
if (chunk !== undefined) {
136-
buffer += chunk
135+
function* processChunk(chunk: StreamChunk | undefined) {
136+
if (chunk !== undefined && chunk.type === 'text') {
137+
buffer += chunk.text
137138
}
138139
extractToolsFromBufferAndProcess()
139140

140141
if (chunk === undefined) {
141142
streamCompleted = true
142143
if (buffer.includes(startToolTag)) {
143144
buffer += completionSuffix
144-
chunk = completionSuffix
145+
chunk = {
146+
type: 'text',
147+
text: completionSuffix,
148+
}
145149
autocompleted = true
146150
}
147151
extractToolsFromBufferAndProcess()
@@ -152,7 +156,7 @@ export async function* processStreamWithTags(
152156
}
153157
}
154158

155-
for await (const chunk of stream as AsyncIterable<string>) {
159+
for await (const chunk of stream) {
156160
if (streamCompleted) {
157161
break
158162
}
@@ -163,7 +167,4 @@ export async function* processStreamWithTags(
163167
// After the stream ends, try parsing one last time in case there's leftover text
164168
yield* processChunk(undefined)
165169
}
166-
167-
for await (const chunk of stream as AsyncIterable<string>) {
168-
}
169170
}

0 commit comments

Comments
 (0)