Skip to content

Commit c710a2d

Browse files
committed
refactor(backend): decouple tool execution from WebSocket layer
Replace direct WebSocket usage in tool handlers and programmatic steps with `sendSubagentChunk` callback function. This improves testability and separates transport concerns from business logic. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent 0cdd977 commit c710a2d

File tree

8 files changed

+29
-73
lines changed

8 files changed

+29
-73
lines changed

backend/src/__tests__/run-programmatic-step.test.ts

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,10 @@ import {
1515
clearAgentGeneratorCache,
1616
runProgrammaticStep,
1717
} from '../run-programmatic-step'
18-
import { mockFileContext, MockWebSocket } from './test-utils'
18+
import { mockFileContext } from './test-utils'
1919
import * as agentRun from '../agent-run'
2020
import * as toolExecutor from '../tools/tool-executor'
2121
import * as requestContext from '../websockets/request-context'
22-
import * as websocketAction from '../websockets/websocket-action'
2322

2423
import type { AgentTemplate, StepGenerator } from '../templates/types'
2524
import type { PublicAgentState } from '@codebuff/common/types/agent-template'
@@ -29,7 +28,7 @@ import type {
2928
} from '@codebuff/common/types/messages/content-part'
3029
import type { AgentState } from '@codebuff/common/types/session-state'
3130
import type { Logger } from '@codebuff/common/types/contracts/logger'
32-
import type { WebSocket } from 'ws'
31+
import type { SendSubagentChunkFn } from '@codebuff/common/types/contracts/client'
3332

3433
const logger: Logger = {
3534
debug: () => {},
@@ -45,7 +44,8 @@ describe('runProgrammaticStep', () => {
4544
let executeToolCallSpy: any
4645
let getRequestContextSpy: any
4746
let addAgentStepSpy: any
48-
let sendActionSpy: any
47+
let sendSubagentChunk: SendSubagentChunkFn
48+
let sentSubagentChunks: Parameters<SendSubagentChunkFn>[0][]
4949

5050
beforeEach(() => {
5151
// Mock analytics
@@ -72,10 +72,10 @@ describe('runProgrammaticStep', () => {
7272
async () => 'test-step-id',
7373
)
7474

75-
// Mock sendAction
76-
sendActionSpy = spyOn(websocketAction, 'sendAction').mockImplementation(
77-
() => {},
78-
)
75+
sentSubagentChunks = []
76+
sendSubagentChunk = (data) => {
77+
sentSubagentChunks.push(data)
78+
}
7979

8080
// Mock crypto.randomUUID
8181
spyOn(crypto, 'randomUUID').mockImplementation(
@@ -132,10 +132,10 @@ describe('runProgrammaticStep', () => {
132132
fileContext: mockFileContext,
133133
assistantMessage: undefined,
134134
assistantPrefix: undefined,
135-
ws: new MockWebSocket() as unknown as WebSocket,
136135
localAgentTemplates: {},
137136
stepsComplete: false,
138137
stepNumber: 1,
138+
sendSubagentChunk,
139139
logger,
140140
}
141141
})
@@ -226,14 +226,6 @@ describe('runProgrammaticStep', () => {
226226
mockTemplate.handleSteps = () => mockGenerator
227227
mockTemplate.toolNames = ['add_message', 'read_files', 'end_turn']
228228

229-
// Track chunks sent via sendSubagentChunk
230-
const sentChunks: string[] = []
231-
sendActionSpy.mockImplementation((ws: any, action: any) => {
232-
if (action.type === 'subagent-response-chunk') {
233-
sentChunks.push(action.chunk)
234-
}
235-
})
236-
237229
const result = await runProgrammaticStep(mockParams)
238230

239231
// Verify add_message tool was executed
@@ -253,15 +245,16 @@ describe('runProgrammaticStep', () => {
253245
)
254246

255247
// Check that no tool call chunk was sent for add_message
256-
const addMessageToolCallChunk = sentChunks.find(
257-
(chunk) =>
248+
const addMessageToolCallChunk = sentSubagentChunks.find(
249+
({ chunk }) =>
258250
chunk.includes('add_message') && chunk.includes('Hello world'),
259251
)
260252
expect(addMessageToolCallChunk).toBeUndefined()
261253

262254
// Check that tool call chunk WAS sent for read_files (normal behavior)
263-
const readFilesToolCallChunk = sentChunks.find(
264-
(chunk) => chunk.includes('read_files') && chunk.includes('test.txt'),
255+
const readFilesToolCallChunk = sentSubagentChunks.find(
256+
({ chunk }) =>
257+
chunk.includes('read_files') && chunk.includes('test.txt'),
265258
)
266259
expect(readFilesToolCallChunk).toBeDefined()
267260

backend/src/__tests__/sandbox-generator.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@ import {
88
clearAgentGeneratorCache,
99
runProgrammaticStep,
1010
} from '../run-programmatic-step'
11-
import { mockFileContext, MockWebSocket } from './test-utils'
11+
import { mockFileContext } from './test-utils'
1212
import * as agentRun from '../agent-run'
1313
import * as requestContext from '../websockets/request-context'
14-
import * as websocketAction from '../websockets/websocket-action'
1514

1615
import type { AgentTemplate } from '../templates/types'
1716
import type { Logger } from '@codebuff/common/types/contracts/logger'
18-
import type { WebSocket } from 'ws'
17+
import type { SendSubagentChunkFn } from '@codebuff/common/types/contracts/client'
1918

2019
const logger: Logger = {
2120
debug: () => {},
@@ -28,6 +27,7 @@ describe('QuickJS Sandbox Generator', () => {
2827
let mockAgentState: AgentState
2928
let mockParams: any
3029
let mockTemplate: AgentTemplate
30+
let sendSubagentChunk: SendSubagentChunkFn
3131

3232
beforeEach(() => {
3333
clearAgentGeneratorCache({ logger })
@@ -39,11 +39,11 @@ describe('QuickJS Sandbox Generator', () => {
3939
spyOn(requestContext, 'getRequestContext').mockImplementation(() => ({
4040
processedRepoId: 'test-repo-id',
4141
}))
42-
spyOn(websocketAction, 'sendAction').mockImplementation(() => {})
4342
spyOn(crypto, 'randomUUID').mockImplementation(
4443
() =>
4544
'mock-uuid-0000-0000-0000-000000000000' as `${string}-${string}-${string}-${string}-${string}`,
4645
)
46+
sendSubagentChunk = () => {}
4747

4848
// Reuse common test data structure
4949
mockAgentState = {
@@ -91,10 +91,10 @@ describe('QuickJS Sandbox Generator', () => {
9191
fileContext: mockFileContext,
9292
assistantMessage: undefined,
9393
assistantPrefix: undefined,
94-
ws: new MockWebSocket() as unknown as WebSocket,
9594
localAgentTemplates: {},
9695
stepsComplete: false,
9796
stepNumber: 1,
97+
sendSubagentChunk,
9898
logger,
9999
}
100100
})

backend/src/__tests__/spawn-agents-permissions.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -739,12 +739,12 @@ describe('Spawn Agents Permissions', () => {
739739
writeToClient: () => {},
740740
getLatestState: () => ({ messages: [] }),
741741
state: {
742-
// Missing required fields like ws, fingerprintId, etc.
742+
// Missing required fields like fingerprintId, messages, etc.
743743
agentTemplate: parentAgent,
744744
localAgentTemplates: {},
745745
},
746746
})
747-
}).toThrow('Missing WebSocket in state')
747+
}).toThrow('Missing fingerprintId in state')
748748
expect(mockLoopAgentSteps).not.toHaveBeenCalled()
749749
})
750750
})

backend/src/run-programmatic-step.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ import { addAgentStep } from './agent-run'
66
import { executeToolCall } from './tools/tool-executor'
77
import { SandboxManager } from './util/quickjs-sandbox'
88
import { getRequestContext } from './websockets/request-context'
9-
import { sendAction } from './websockets/websocket-action'
109

1110
import type { CodebuffToolCall } from '@codebuff/common/tools/list'
1211
import type {
1312
AgentTemplate,
1413
StepGenerator,
1514
PublicAgentState,
1615
} from '@codebuff/common/types/agent-template'
17-
import type { HandleStepsLogChunkFn } from '@codebuff/common/types/contracts/client'
16+
import type {
17+
HandleStepsLogChunkFn,
18+
SendSubagentChunkFn,
19+
} from '@codebuff/common/types/contracts/client'
1820
import type { Logger } from '@codebuff/common/types/contracts/logger'
1921
import type {
2022
ParamsExcluding,
@@ -26,7 +28,6 @@ import type {
2628
} from '@codebuff/common/types/messages/content-part'
2729
import type { PrintModeEvent } from '@codebuff/common/types/print-mode'
2830
import type { AgentState } from '@codebuff/common/types/session-state'
29-
import type { WebSocket } from 'ws'
3031

3132
// Global sandbox manager for QuickJS contexts
3233
const sandboxManager = new SandboxManager()
@@ -59,10 +60,10 @@ export async function runProgrammaticStep(
5960
userInputId: string
6061
fingerprintId: string
6162
onResponseChunk: (chunk: string | PrintModeEvent) => void
62-
ws: WebSocket
6363
localAgentTemplates: Record<string, AgentTemplate>
6464
stepsComplete: boolean
6565
stepNumber: number
66+
sendSubagentChunk: SendSubagentChunkFn | undefined
6667
handleStepsLogChunk: HandleStepsLogChunkFn
6768
logger: Logger
6869
} & ParamsExcluding<
@@ -91,9 +92,9 @@ export async function runProgrammaticStep(
9192
userInputId,
9293
fingerprintId,
9394
onResponseChunk,
94-
ws,
9595
localAgentTemplates,
9696
stepsComplete,
97+
sendSubagentChunk,
9798
handleStepsLogChunk,
9899
logger,
99100
} = params
@@ -179,25 +180,13 @@ export async function runProgrammaticStep(
179180
const toolCalls: CodebuffToolCall[] = []
180181
const toolResults: ToolResultPart[] = []
181182
const state = {
182-
ws,
183183
fingerprintId,
184184
userId,
185185
repoId,
186186
agentTemplate: template,
187187
localAgentTemplates,
188188
system,
189-
sendSubagentChunk: (data: {
190-
userInputId: string
191-
agentId: string
192-
agentType: string
193-
chunk: string
194-
prompt?: string
195-
}) => {
196-
sendAction(ws, {
197-
type: 'subagent-response-chunk',
198-
...data,
199-
})
200-
},
189+
sendSubagentChunk,
201190
agentState: cloneDeep({
202191
...agentState,
203192
runId: agentState.runId!, // We've already verified runId exists above

backend/src/tools/handlers/tool/read-files.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import type {
99
import type { ParamsExcluding } from '@codebuff/common/types/function-params'
1010
import type { Message } from '@codebuff/common/types/messages/codebuff-message'
1111
import type { ProjectFileContext } from '@codebuff/common/util/file'
12-
import type { WebSocket } from 'ws'
13-
1412
type ToolName = 'read_files'
1513
export const handleReadFiles = ((
1614
params: {
@@ -21,7 +19,6 @@ export const handleReadFiles = ((
2119
fileContext: ProjectFileContext
2220

2321
state: {
24-
ws?: WebSocket
2522
userId?: string
2623
fingerprintId?: string
2724
repoId?: string
@@ -39,11 +36,8 @@ export const handleReadFiles = ((
3936
fileContext,
4037
state,
4138
} = params
42-
const { ws, fingerprintId, userId, repoId, messages } = state
39+
const { fingerprintId, userId, repoId, messages } = state
4340
const { paths } = toolCall.input
44-
if (!ws) {
45-
throw new Error('Internal error for read_files: Missing WebSocket in state')
46-
}
4741
if (!messages) {
4842
throw new Error('Internal error for read_files: Missing messages in state')
4943
}

backend/src/tools/handlers/tool/spawn-agent-inline.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import type { AgentState } from '@codebuff/common/types/session-state'
1919
import type { ProjectFileContext } from '@codebuff/common/util/file'
2020
import type { ParamsExcluding } from '@codebuff/common/types/function-params'
2121
import type { Logger } from '@codebuff/common/types/contracts/logger'
22-
import type { WebSocket } from 'ws'
2322

2423
type ToolName = 'spawn_agent_inline'
2524
export const handleSpawnAgentInline = ((
@@ -33,7 +32,6 @@ export const handleSpawnAgentInline = ((
3332

3433
getLatestState: () => { messages: Message[] }
3534
state: {
36-
ws?: WebSocket
3735
fingerprintId?: string
3836
userId?: string
3937
agentTemplate?: AgentTemplate
@@ -56,7 +54,6 @@ export const handleSpawnAgentInline = ((
5654
| 'parentSystemPrompt'
5755
| 'onResponseChunk'
5856
| 'clearUserPromptMessagesAfterResponse'
59-
| 'ws'
6057
| 'fingerprintId'
6158
>,
6259
): { result: Promise<CodebuffToolOutput<ToolName>>; state: {} } => {
@@ -74,7 +71,6 @@ export const handleSpawnAgentInline = ((
7471
params: spawnParams,
7572
} = toolCall.input
7673
const {
77-
ws,
7874
fingerprintId,
7975
userId,
8076
agentTemplate: parentAgentTemplate,
@@ -115,7 +111,6 @@ export const handleSpawnAgentInline = ((
115111

116112
const result = await executeSubagent({
117113
...params,
118-
ws,
119114
userInputId: `${userInputId}-inline-${agentType}${childAgentState.agentId}`,
120115
prompt: prompt || '',
121116
spawnParams,

backend/src/tools/handlers/tool/spawn-agent-utils.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,13 @@ import type {
1919
Subgoal,
2020
} from '@codebuff/common/types/session-state'
2121
import type { ProjectFileContext } from '@codebuff/common/util/file'
22-
import type { WebSocket } from 'ws'
23-
2422
export interface SpawnAgentParams {
2523
agent_type: string
2624
prompt?: string
2725
params?: any
2826
}
2927

3028
export interface BaseSpawnState {
31-
ws?: WebSocket
3229
fingerprintId?: string
3330
userId?: string
3431
agentTemplate?: AgentTemplate
@@ -53,7 +50,6 @@ export function validateSpawnState(
5350
toolName: string,
5451
): Omit<Required<BaseSpawnState>, 'userId'> & { userId: string | undefined } {
5552
const {
56-
ws,
5753
fingerprintId,
5854
agentTemplate: parentAgentTemplate,
5955
localAgentTemplates,
@@ -63,11 +59,6 @@ export function validateSpawnState(
6359
system,
6460
} = state
6561

66-
if (!ws) {
67-
throw new Error(
68-
`Internal error for ${toolName}: Missing WebSocket in state`,
69-
)
70-
}
7162
if (!fingerprintId) {
7263
throw new Error(
7364
`Internal error for ${toolName}: Missing fingerprintId in state`,
@@ -96,7 +87,6 @@ export function validateSpawnState(
9687
}
9788

9889
return {
99-
ws,
10090
fingerprintId,
10191
userId,
10292
agentTemplate: parentAgentTemplate,

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import type { ParamsExcluding } from '@codebuff/common/types/function-params'
1818
import type { Message } from '@codebuff/common/types/messages/codebuff-message'
1919
import type { PrintModeEvent } from '@codebuff/common/types/print-mode'
2020
import type { AgentState } from '@codebuff/common/types/session-state'
21-
import type { WebSocket } from 'ws'
2221

2322
export type SendSubagentChunk = (data: {
2423
userInputId: string
@@ -40,7 +39,6 @@ export const handleSpawnAgents = ((
4039

4140
getLatestState: () => { messages: Message[] }
4241
state: {
43-
ws?: WebSocket
4442
fingerprintId?: string
4543
userId?: string
4644
agentTemplate?: AgentTemplate
@@ -57,7 +55,6 @@ export const handleSpawnAgents = ((
5755
> &
5856
ParamsExcluding<
5957
typeof executeSubagent,
60-
| 'ws'
6158
| 'userInputId'
6259
| 'prompt'
6360
| 'spawnParams'
@@ -93,7 +90,6 @@ export const handleSpawnAgents = ((
9390
}
9491

9592
const {
96-
ws,
9793
fingerprintId,
9894
userId,
9995
agentTemplate: parentAgentTemplate,
@@ -135,7 +131,6 @@ export const handleSpawnAgents = ((
135131

136132
const result = await executeSubagent({
137133
...params,
138-
ws,
139134
userInputId: `${userInputId}-${agentType}${subAgentState.agentId}`,
140135
prompt: prompt || '',
141136
spawnParams,

0 commit comments

Comments
 (0)