Skip to content

Commit 9026052

Browse files
[feat] initial cli app with proper tui (#339)
Co-authored-by: Codebuff <[email protected]>
1 parent 58e5fd4 commit 9026052

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+9209
-351
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dist-env
1818
tsconfig.tsbuildinfo
1919
.manicode
2020
__mock-projects__
21+
npm-app/src/__tests__/data/
2122
.aider*
2223
.codebuff*
2324
**.log

backend/src/__tests__/subagent-streaming.test.ts

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -173,32 +173,20 @@ describe('Subagent Streaming', () => {
173173
await result
174174

175175
// Verify that subagent streaming messages were sent
176-
expect(mockWriteToClient).toHaveBeenCalledTimes(4)
176+
expect(mockWriteToClient).toHaveBeenCalledTimes(2)
177177

178-
// First streaming chunk is a labled divider
179-
expect(mockWriteToClient).toHaveBeenNthCalledWith(1, {
180-
type: 'subagent_start',
181-
agentId: 'thinker',
182-
displayName: 'Thinker',
183-
onlyChild: true,
184-
})
178+
// First call is subagent_start
179+
expect(mockWriteToClient).toHaveBeenNthCalledWith(
180+
1,
181+
expect.objectContaining({ type: 'subagent_start' }),
182+
)
185183

186-
// Check first streaming chunk
184+
// Second call is subagent_finish
187185
expect(mockWriteToClient).toHaveBeenNthCalledWith(
188186
2,
189-
'Thinking about the problem...',
187+
expect.objectContaining({ type: 'subagent_finish' }),
190188
)
191-
192-
// Check second streaming chunk
193-
expect(mockWriteToClient).toHaveBeenNthCalledWith(3, 'Found a solution!')
194-
195-
// Last streaming chunk is a labeled divider
196-
expect(mockWriteToClient).toHaveBeenNthCalledWith(4, {
197-
type: 'subagent_finish',
198-
agentId: 'thinker',
199-
displayName: 'Thinker',
200-
onlyChild: true,
201-
})
189+
return
202190
})
203191

204192
it('should include correct agentId and agentType in streaming messages', async () => {

backend/src/client-wrapper.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,15 @@ export function sendSubagentChunkWs(
207207
ws: WebSocket
208208
} & ParamsOf<SendSubagentChunkFn>,
209209
): ReturnType<SendSubagentChunkFn> {
210-
const { ws, userInputId, agentId, agentType, chunk, prompt } = params
210+
const {
211+
ws,
212+
userInputId,
213+
agentId,
214+
agentType,
215+
chunk,
216+
prompt,
217+
forwardToPrompt = true,
218+
} = params
211219
return sendActionWs({
212220
ws,
213221
action: {
@@ -217,6 +225,7 @@ export function sendSubagentChunkWs(
217225
agentType,
218226
chunk,
219227
prompt,
228+
forwardToPrompt,
220229
},
221230
})
222231
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ import type {
3030
import type { LanguageModel } from 'ai'
3131
import type { z } from 'zod/v4'
3232

33+
export type StreamChunk =
34+
| {
35+
type: 'text'
36+
text: string
37+
agentId?: string
38+
}
39+
| {
40+
type: 'reasoning'
41+
text: string
42+
}
43+
| { type: 'error'; message: string }
3344
// TODO: We'll want to add all our models here!
3445
const modelToAiSDKModel = (model: Model): LanguageModel => {
3546
if (
@@ -56,6 +67,9 @@ export async function* promptAiSdkStream(
5667
params: ParamsOf<PromptAiSdkStreamFn>,
5768
): ReturnType<PromptAiSdkStreamFn> {
5869
const { logger } = params
70+
const agentChunkMetadata =
71+
params.agentId != null ? { agentId: params.agentId } : undefined
72+
5973
if (
6074
!checkLiveUserInput({ ...params, clientSessionId: params.clientSessionId })
6175
) {
@@ -91,6 +105,7 @@ export async function* promptAiSdkStream(
91105
yield {
92106
type: 'text',
93107
text: flushed,
108+
...(agentChunkMetadata ?? {}),
94109
}
95110
}
96111
}
@@ -143,6 +158,7 @@ export async function* promptAiSdkStream(
143158
yield {
144159
type: 'text',
145160
text: chunk.text,
161+
...(agentChunkMetadata ?? {}),
146162
}
147163
}
148164
continue
@@ -154,6 +170,7 @@ export async function* promptAiSdkStream(
154170
yield {
155171
type: 'text',
156172
text: stopSequenceResult.text,
173+
...(agentChunkMetadata ?? {}),
157174
}
158175
}
159176
}
@@ -164,6 +181,7 @@ export async function* promptAiSdkStream(
164181
yield {
165182
type: 'text',
166183
text: flushed,
184+
...(agentChunkMetadata ?? {}),
167185
}
168186
}
169187

backend/src/run-agent-step.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import type {
3333
FinishAgentRunFn,
3434
StartAgentRunFn,
3535
} from '@codebuff/common/types/contracts/database'
36+
import type { SendActionFn } from '@codebuff/common/types/contracts/client'
3637
import type { Logger } from '@codebuff/common/types/contracts/logger'
3738
import type { ParamsExcluding } from '@codebuff/common/types/function-params'
3839
import type { Message } from '@codebuff/common/types/messages/codebuff-message'
@@ -56,6 +57,7 @@ export const runAgentStep = async (
5657
clientSessionId: string
5758
fingerprintId: string
5859
onResponseChunk: (chunk: string | PrintModeEvent) => void
60+
sendAction: SendActionFn
5961

6062
agentType: AgentTemplateType
6163
fileContext: ProjectFileContext
@@ -105,13 +107,15 @@ export const runAgentStep = async (
105107
fingerprintId,
106108
clientSessionId,
107109
onResponseChunk,
110+
sendAction,
108111
fileContext,
109112
agentType,
110113
localAgentTemplates,
111114
prompt,
112115
spawnParams,
113116
system,
114117
logger,
118+
promptAiSdkStream,
115119
} = params
116120
let agentState = params.agentState
117121

@@ -234,8 +238,11 @@ export const runAgentStep = async (
234238
const { model } = agentTemplate
235239

236240
const { getStream } = getAgentStreamFromTemplate({
237-
...params,
238-
agentId: agentState.agentId,
241+
clientSessionId,
242+
fingerprintId,
243+
userInputId,
244+
userId,
245+
agentId: agentState.parentId ? agentState.agentId : undefined,
239246
template: agentTemplate,
240247
onCostCalculated: async (credits: number) => {
241248
try {
@@ -254,6 +261,9 @@ export const runAgentStep = async (
254261
)
255262
}
256263
},
264+
sendAction,
265+
promptAiSdkStream,
266+
logger,
257267
includeCacheControl: supportsCacheControl(agentTemplate.model),
258268
})
259269

backend/src/run-programmatic-step.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ export async function runProgrammaticStep(
192192
agentType: string
193193
chunk: string
194194
prompt?: string
195+
forwardToPrompt?: boolean
195196
}) => {
196197
sendAction({
197198
action: {
@@ -275,15 +276,18 @@ export async function runProgrammaticStep(
275276
role: 'assistant' as const,
276277
content: toolCallString,
277278
})
278-
state.sendSubagentChunk({
279+
// Optional call handles both top-level and nested agents
280+
state.sendSubagentChunk?.({
279281
userInputId,
280282
agentId: state.agentState.agentId,
281283
agentType: state.agentState.agentType!,
282284
chunk: toolCallString,
285+
forwardToPrompt: !state.agentState.parentId,
283286
})
284287
}
285288

286289
// Execute the tool synchronously and get the result immediately
290+
// Wrap onResponseChunk to add parentAgentId to nested agent events
287291
await executeToolCall({
288292
...params,
289293
toolName: toolCall.toolName,
@@ -299,6 +303,70 @@ export async function runProgrammaticStep(
299303
autoInsertEndStepParam: true,
300304
excludeToolFromMessageHistory,
301305
fromHandleSteps: true,
306+
onResponseChunk: (chunk: string | PrintModeEvent) => {
307+
if (typeof chunk === 'string') {
308+
onResponseChunk(chunk)
309+
return
310+
}
311+
312+
// Only add parentAgentId if this programmatic agent has a parent (i.e., it's nested)
313+
// This ensures we don't add parentAgentId to top-level spawns
314+
if (state.agentState.parentId) {
315+
const parentAgentId = state.agentState.agentId
316+
317+
switch (chunk.type) {
318+
case 'subagent_start':
319+
case 'subagent_finish':
320+
if (!chunk.parentAgentId) {
321+
logger.debug(
322+
{
323+
eventType: chunk.type,
324+
agentId: chunk.agentId,
325+
parentId: parentAgentId,
326+
},
327+
`run-programmatic-step: Adding parentAgentId to ${chunk.type} event`,
328+
)
329+
onResponseChunk({
330+
...chunk,
331+
parentAgentId,
332+
})
333+
return
334+
}
335+
break
336+
case 'tool_call':
337+
case 'tool_result': {
338+
if (!chunk.parentAgentId) {
339+
const debugPayload =
340+
chunk.type === 'tool_call'
341+
? {
342+
eventType: chunk.type,
343+
agentId: chunk.agentId,
344+
parentId: parentAgentId,
345+
}
346+
: {
347+
eventType: chunk.type,
348+
parentId: parentAgentId,
349+
}
350+
logger.debug(
351+
debugPayload,
352+
`run-programmatic-step: Adding parentAgentId to ${chunk.type} event`,
353+
)
354+
onResponseChunk({
355+
...chunk,
356+
parentAgentId,
357+
})
358+
return
359+
}
360+
break
361+
}
362+
default:
363+
break
364+
}
365+
}
366+
367+
// For other events or top-level spawns, send as-is
368+
onResponseChunk(chunk)
369+
},
302370
})
303371

304372
// TODO: Remove messages from state and always use agentState.messageHistory.

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -323,12 +323,15 @@ export async function executeSubagent(
323323
const { onResponseChunk, agentTemplate, parentAgentState, isOnlyChild } =
324324
withDefaults
325325

326-
onResponseChunk({
327-
type: 'subagent_start',
328-
agentId: agentTemplate.id,
326+
const startEvent = {
327+
type: 'subagent_start' as const,
328+
agentId: withDefaults.agentState.agentId,
329+
agentType: agentTemplate.id,
329330
displayName: agentTemplate.displayName,
330331
onlyChild: isOnlyChild,
331-
})
332+
parentAgentId: parentAgentState.agentId,
333+
}
334+
onResponseChunk(startEvent)
332335

333336
// Import loopAgentSteps dynamically to avoid circular dependency
334337
const { loopAgentSteps } = await import('../../../run-agent-step')
@@ -340,9 +343,11 @@ export async function executeSubagent(
340343

341344
onResponseChunk({
342345
type: 'subagent_finish',
343-
agentId: agentTemplate.id,
346+
agentId: result.agentState.agentId,
347+
agentType: agentTemplate.id,
344348
displayName: agentTemplate.displayName,
345349
onlyChild: isOnlyChild,
350+
parentAgentId: parentAgentState.agentId,
346351
})
347352

348353
if (result.agentState.runId) {

0 commit comments

Comments
 (0)