Skip to content

Commit c7ddc7e

Browse files
authored
Print mode JSON streaming (#223)
1 parent a3444ba commit c7ddc7e

File tree

21 files changed

+479
-204
lines changed

21 files changed

+479
-204
lines changed

backend/src/__tests__/main-prompt.integration.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { mainPrompt } from '../main-prompt'
1414

1515
// Mock imports needed for setup within the test
1616
import { getToolCallString } from '@codebuff/common/constants/tools'
17+
import { PrintModeObject } from '@codebuff/common/types/print-mode'
1718
import { ProjectFileContext } from '@codebuff/common/util/file'
1819
import * as checkTerminalCommandModule from '../check-terminal-command'
1920
import * as requestFilesPrompt from '../find-files/request-files-prompt'
@@ -363,7 +364,10 @@ export function getMessagesSubset(messages: Message[], otherTokens: number) {
363364
} = await mainPrompt(new MockWebSocket() as unknown as WebSocket, action, {
364365
userId: TEST_USER_ID,
365366
clientSessionId: 'test-session-delete-function-integration',
366-
onResponseChunk: (chunk: string) => {
367+
onResponseChunk: (chunk: string | PrintModeObject) => {
368+
if (typeof chunk !== 'string') {
369+
return
370+
}
367371
process.stdout.write(chunk)
368372
},
369373
})
@@ -444,7 +448,10 @@ export function getMessagesSubset(messages: Message[], otherTokens: number) {
444448
await mainPrompt(new MockWebSocket() as unknown as WebSocket, action, {
445449
userId: TEST_USER_ID,
446450
clientSessionId: 'test-session-delete-function-integration',
447-
onResponseChunk: (chunk: string) => {
451+
onResponseChunk: (chunk: string | PrintModeObject) => {
452+
if (typeof chunk !== 'string') {
453+
return
454+
}
448455
process.stdout.write(chunk)
449456
},
450457
})

backend/src/main-prompt.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
import { WebSocket } from 'ws'
1010

1111
import { renderToolResults } from '@codebuff/common/constants/tools'
12+
import { PrintModeObject } from '@codebuff/common/types/print-mode'
1213
import { checkTerminalCommand } from './check-terminal-command'
1314
import { loopAgentSteps } from './run-agent-step'
1415
import { getAllAgentTemplates } from './templates/agent-registry'
@@ -20,7 +21,7 @@ import { requestToolCall } from './websockets/websocket-action'
2021
export interface MainPromptOptions {
2122
userId: string | undefined
2223
clientSessionId: string
23-
onResponseChunk: (chunk: string) => void
24+
onResponseChunk: (chunk: string | PrintModeObject) => void
2425
}
2526

2627
export const mainPrompt = async (

backend/src/run-agent-step.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
renderToolResults,
1111
} from '@codebuff/common/constants/tools'
1212
import { CodebuffMessage } from '@codebuff/common/types/message'
13+
import { PrintModeObject } from '@codebuff/common/types/print-mode'
1314
import {
1415
AgentState,
1516
ToolResult,
@@ -50,7 +51,7 @@ export interface AgentOptions {
5051
userInputId: string
5152
clientSessionId: string
5253
fingerprintId: string
53-
onResponseChunk: (chunk: string) => void
54+
onResponseChunk: (chunk: string | PrintModeObject) => void
5455

5556
agentType: AgentTemplateType
5657
fileContext: ProjectFileContext
@@ -480,7 +481,20 @@ export const runAgentStep = async (
480481

481482
export const loopAgentSteps = async (
482483
ws: WebSocket,
483-
options: {
484+
{
485+
userInputId,
486+
agentType,
487+
agentState,
488+
prompt,
489+
params,
490+
fingerprintId,
491+
fileContext,
492+
toolResults,
493+
agentRegistry,
494+
userId,
495+
clientSessionId,
496+
onResponseChunk,
497+
}: {
484498
userInputId: string
485499
agentType: AgentTemplateType
486500
agentState: AgentState
@@ -493,22 +507,9 @@ export const loopAgentSteps = async (
493507

494508
userId: string | undefined
495509
clientSessionId: string
496-
onResponseChunk: (chunk: string) => void
510+
onResponseChunk: (chunk: string | PrintModeObject) => void
497511
}
498512
) => {
499-
const {
500-
agentState,
501-
prompt,
502-
params,
503-
userId,
504-
clientSessionId,
505-
agentRegistry,
506-
onResponseChunk,
507-
userInputId,
508-
fingerprintId,
509-
fileContext,
510-
agentType,
511-
} = options
512513
const agentTemplate = agentRegistry[agentType]
513514
if (!agentTemplate) {
514515
throw new Error(`Agent template not found for type: ${agentType}`)

backend/src/run-programmatic-step.ts

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { PrintModeObject } from '@codebuff/common/types/print-mode'
12
import {
23
AgentState,
34
AgentTemplateType,
@@ -37,40 +38,44 @@ export function clearAgentGeneratorCache() {
3738
// Function to handle programmatic agents
3839
export async function runProgrammaticStep(
3940
agentState: AgentState,
40-
params: {
41+
{
42+
template,
43+
prompt,
44+
params,
45+
userId,
46+
userInputId,
47+
clientSessionId,
48+
fingerprintId,
49+
onResponseChunk,
50+
agentType,
51+
fileContext,
52+
assistantMessage,
53+
assistantPrefix,
54+
ws,
55+
}: {
4156
template: AgentTemplate
4257
prompt: string | undefined
4358
params: Record<string, any> | undefined
4459
userId: string | undefined
4560
userInputId: string
4661
clientSessionId: string
4762
fingerprintId: string
48-
onResponseChunk: (chunk: string) => void
63+
onResponseChunk: (chunk: string | PrintModeObject) => void
4964
agentType: AgentTemplateType
5065
fileContext: ProjectFileContext
5166
ws: WebSocket
5267
}
5368
): Promise<{ agentState: AgentState; endTurn: boolean }> {
54-
const {
55-
template,
56-
onResponseChunk,
57-
ws,
58-
userId,
59-
userInputId,
60-
clientSessionId,
61-
fingerprintId,
62-
fileContext,
63-
} = params
6469
if (!template.handleSteps) {
6570
throw new Error('No step handler found for agent template ' + template.id)
6671
}
6772

6873
logger.info(
6974
{
7075
template: template.id,
71-
agentType: params.agentType,
72-
prompt: params.prompt,
73-
params: params.params,
76+
agentType,
77+
prompt,
78+
params,
7479
},
7580
'Running programmatic step'
7681
)
@@ -88,16 +93,16 @@ export async function runProgrammaticStep(
8893
template.handleSteps,
8994
{
9095
agentState,
91-
prompt: params.prompt,
92-
params: params.params,
96+
prompt,
97+
params,
9398
}
9499
)
95100
} else {
96101
// Initialize native generator
97102
generator = template.handleSteps({
98103
agentState,
99-
prompt: params.prompt,
100-
params: params.params,
104+
prompt,
105+
params,
101106
})
102107
agentIdToGenerator[agentState.agentId] = generator
103108
}

backend/src/tools/handlers/tool/spawn-agents-async.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { CodebuffToolHandlerFunction } from '../handler-function-type'
1212
import type { SendSubagentChunk } from './spawn-agents'
1313

1414
import { ASYNC_AGENTS_ENABLED } from '@codebuff/common/constants'
15+
import { PrintModeObject } from '@codebuff/common/types/print-mode'
1516
import { generateCompactId } from '@codebuff/common/util/string'
1617
import { asyncAgentManager } from '../../../async-agent-manager'
1718
import { getAllAgentTemplates } from '../../../templates/agent-registry'
@@ -197,14 +198,18 @@ export const handleSpawnAgentsAsync = ((params: {
197198
toolResults: [],
198199
userId,
199200
clientSessionId,
200-
onResponseChunk: (chunk: string) =>
201+
onResponseChunk: (chunk: string | PrintModeObject) => {
202+
if (typeof chunk !== 'string') {
203+
return
204+
}
201205
sendSubagentChunk({
202206
userInputId,
203207
agentId,
204208
agentType,
205209
chunk,
206210
prompt,
207-
}),
211+
})
212+
},
208213
})
209214

210215
// Send completion message to parent if agent has appropriate output mode

backend/src/tools/handlers/tool/spawn-agents.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { AgentTemplate } from '../../../templates/types'
1010
import type { CodebuffToolCall } from '../../constants'
1111
import type { CodebuffToolHandlerFunction } from '../handler-function-type'
1212

13+
import { PrintModeObject } from '@codebuff/common/types/print-mode'
1314
import { generateCompactId } from '@codebuff/common/util/string'
1415
import { getAllAgentTemplates } from '../../../templates/agent-registry'
1516
import { logger } from '../../../util/logger'
@@ -193,7 +194,10 @@ export const handleSpawnAgents = ((params: {
193194
toolResults: [],
194195
userId,
195196
clientSessionId,
196-
onResponseChunk: (chunk: string) => {
197+
onResponseChunk: (chunk: string | PrintModeObject) => {
198+
if (typeof chunk !== 'string') {
199+
return
200+
}
197201
// Send subagent streaming chunks to client
198202
sendSubagentChunk({
199203
userInputId,

backend/src/tools/stream-parser.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1-
import { ToolName, toolNames } from '@codebuff/common/constants/tools'
2-
import { CodebuffMessage } from '@codebuff/common/types/message'
3-
import {
1+
import type { ToolName } from '@codebuff/common/constants/tools'
2+
import type { CodebuffMessage } from '@codebuff/common/types/message'
3+
import type { PrintModeObject } from '@codebuff/common/types/print-mode'
4+
import type {
45
AgentState,
56
Subgoal,
67
ToolResult,
78
} from '@codebuff/common/types/session-state'
9+
import type { ProjectFileContext } from '@codebuff/common/util/file'
10+
import type { ToolCallPart } from 'ai'
11+
import type { WebSocket } from 'ws'
12+
import type { AgentTemplate } from '../templates/types'
13+
import type { CodebuffToolCall } from './constants'
14+
15+
import { toolNames } from '@codebuff/common/constants/tools'
816
import { buildArray } from '@codebuff/common/util/array'
9-
import { ProjectFileContext } from '@codebuff/common/util/file'
1017
import { generateCompactId } from '@codebuff/common/util/string'
11-
import { ToolCallPart } from 'ai'
12-
import { WebSocket } from 'ws'
13-
import { AgentTemplate } from '../templates/types'
1418
import { expireMessages } from '../util/messages'
1519
import { sendAction } from '../websockets/websocket-action'
1620
import { processStreamWithTags } from '../xml-stream-parser'
17-
import { CodebuffToolCall } from './constants'
1821
import { executeToolCall } from './tool-executor'
1922

2023
export type ToolCallError = {
@@ -37,7 +40,7 @@ export async function processStreamWithTools<T extends string>(options: {
3740
messages: CodebuffMessage[]
3841
agentState: AgentState
3942
agentContext: Record<string, Subgoal>
40-
onResponseChunk: (chunk: string) => void
43+
onResponseChunk: (chunk: string | PrintModeObject) => void
4144
fullResponse: string
4245
}) {
4346
const {
@@ -126,6 +129,7 @@ export async function processStreamWithTools<T extends string>(options: {
126129
result: error,
127130
})
128131
},
132+
onResponseChunk,
129133
{
130134
userId,
131135
model: agentTemplate.model,

backend/src/tools/tool-executor.ts

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ToolName } from '@codebuff/common/constants/tools'
2+
import type { PrintModeObject } from '@codebuff/common/types/print-mode'
23
import type { ToolResult } from '@codebuff/common/types/session-state'
34
import type { ProjectFileContext } from '@codebuff/common/util/file'
45
import type { WebSocket } from 'ws'
@@ -105,34 +106,30 @@ export interface ExecuteToolCallParams<T extends ToolName = ToolName> {
105106
clientSessionId: string
106107
userInputId: string
107108
fullResponse: string
108-
onResponseChunk: (chunk: string) => void
109+
onResponseChunk: (chunk: string | PrintModeObject) => void
109110
state: Record<string, any>
110111
userId: string | undefined
111112
autoInsertEndStepParam?: boolean
112113
}
113114

114-
export function executeToolCall<T extends ToolName>(
115-
options: ExecuteToolCallParams<T>
116-
): Promise<void> {
117-
const {
118-
toolName,
119-
args,
120-
toolCalls,
121-
toolResults,
122-
previousToolCallFinished,
123-
ws,
124-
agentTemplate,
125-
fileContext,
126-
agentStepId,
127-
clientSessionId,
128-
userInputId,
129-
fullResponse,
130-
onResponseChunk,
131-
state,
132-
userId,
133-
autoInsertEndStepParam = false,
134-
} = options
135-
115+
export function executeToolCall<T extends ToolName>({
116+
toolName,
117+
args,
118+
toolCalls,
119+
toolResults,
120+
previousToolCallFinished,
121+
ws,
122+
agentTemplate,
123+
fileContext,
124+
agentStepId,
125+
clientSessionId,
126+
userInputId,
127+
fullResponse,
128+
onResponseChunk,
129+
state,
130+
userId,
131+
autoInsertEndStepParam = false,
132+
}: ExecuteToolCallParams<T>): Promise<void> {
136133
const toolCall: CodebuffToolCall<T> | ToolCallError = parseRawToolCall<T>(
137134
{
138135
toolName,
@@ -154,6 +151,13 @@ export function executeToolCall<T extends ToolName>(
154151
return previousToolCallFinished
155152
}
156153

154+
onResponseChunk({
155+
type: 'tool_call',
156+
toolCallId: toolCall.toolCallId,
157+
toolName,
158+
args: toolCall.args,
159+
})
160+
157161
logger.debug(
158162
{ toolCall },
159163
`${toolName} (${toolCall.toolCallId}) tool call detected in stream`
@@ -226,6 +230,12 @@ export function executeToolCall<T extends ToolName>(
226230
return
227231
}
228232

233+
onResponseChunk({
234+
type: 'tool_result',
235+
toolCallId: toolResult.toolCallId,
236+
result: toolResult.result,
237+
})
238+
229239
toolResults.push(toolResult)
230240

231241
state.messages.push({

backend/src/websockets/websocket-action.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,11 @@ const onPrompt = async (
181181
}
182182
)
183183

184+
sendAction(ws, {
185+
type: 'response-chunk',
186+
userInputId: promptId,
187+
chunk: { type: 'error', message: response },
188+
})
184189
sendAction(ws, {
185190
type: 'response-chunk',
186191
userInputId: promptId,

0 commit comments

Comments
 (0)