Skip to content

Commit e560c6a

Browse files
committed
Add AgentOutput schema; loopAgentSteps returns it. SDK returns it with session state
1 parent babb9c8 commit e560c6a

16 files changed

+172
-145
lines changed

backend/src/__tests__/cost-aggregation.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ describe('Cost Aggregation System', () => {
137137
stepsRemaining: 10,
138138
creditsUsed: 75, // First subagent uses 75 credits
139139
},
140+
output: { type: 'lastMessage', value: 'Sub-agent 1 response' },
140141
})
141142
.mockResolvedValueOnce({
142143
agentState: {
@@ -148,6 +149,7 @@ describe('Cost Aggregation System', () => {
148149
stepsRemaining: 10,
149150
creditsUsed: 100, // Second subagent uses 100 credits
150151
},
152+
output: { type: 'lastMessage', value: 'Sub-agent 2 response' },
151153
})
152154

153155
const mockToolCall = {
@@ -213,6 +215,7 @@ describe('Cost Aggregation System', () => {
213215
stepsRemaining: 10,
214216
creditsUsed: 50, // Successful agent
215217
},
218+
output: { type: 'lastMessage', value: 'Successful response' },
216219
})
217220
.mockRejectedValueOnce((() => {
218221
const error = new Error('Agent failed') as any
@@ -225,6 +228,7 @@ describe('Cost Aggregation System', () => {
225228
stepsRemaining: 10,
226229
creditsUsed: 25, // Partial cost from failed agent
227230
}
231+
error.output = { type: 'error', message: 'Agent failed' }
228232
return error
229233
})())
230234

@@ -366,6 +370,7 @@ describe('Cost Aggregation System', () => {
366370
stepsRemaining: 10,
367371
creditsUsed: subAgent1Cost,
368372
} as AgentState,
373+
output: { type: 'lastMessage', value: 'Sub-agent 1 response' },
369374
})
370375
.mockResolvedValueOnce({
371376
agentState: {
@@ -377,6 +382,7 @@ describe('Cost Aggregation System', () => {
377382
stepsRemaining: 10,
378383
creditsUsed: subAgent2Cost,
379384
} as AgentState,
385+
output: { type: 'lastMessage', value: 'Sub-agent 2 response' },
380386
})
381387

382388
const mockToolCall = {

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -384,8 +384,7 @@ export function getMessagesSubset(messages: Message[], otherTokens: number) {
384384
}
385385

386386
const {
387-
toolCalls,
388-
toolResults,
387+
output,
389388
sessionState: finalSessionState,
390389
} = await mainPrompt(new MockWebSocket() as unknown as WebSocket, action, {
391390
userId: TEST_USER_ID,

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ describe('mainPrompt', () => {
335335
toolResults: [],
336336
}
337337

338-
const { toolCalls, sessionState: newSessionState } = await mainPrompt(
338+
const { sessionState: newSessionState, output } = await mainPrompt(
339339
new MockWebSocket() as unknown as WebSocket,
340340
action,
341341
{
@@ -361,6 +361,9 @@ describe('mainPrompt', () => {
361361
}),
362362
)
363363

364+
// Verify that the output contains the expected structure
365+
expect(output.type).toBeDefined()
366+
364367
// Verify that a tool result was added to message history
365368
const toolResultMessages =
366369
newSessionState.mainAgentState.messageHistory.filter(
@@ -466,7 +469,7 @@ describe('mainPrompt', () => {
466469
toolResults: [],
467470
}
468471

469-
const { toolCalls } = await mainPrompt(
472+
const { output } = await mainPrompt(
470473
new MockWebSocket() as unknown as WebSocket,
471474
action,
472475
{
@@ -477,7 +480,7 @@ describe('mainPrompt', () => {
477480
},
478481
)
479482

480-
expect(toolCalls).toHaveLength(0) // No tool calls expected
483+
expect(output.type).toBeDefined() // Output should exist
481484
})
482485

483486
it('should update consecutiveAssistantMessages when new prompt is received', async () => {
@@ -556,7 +559,7 @@ describe('mainPrompt', () => {
556559
toolResults: [],
557560
}
558561

559-
const { toolCalls } = await mainPrompt(
562+
const { output } = await mainPrompt(
560563
new MockWebSocket() as unknown as WebSocket,
561564
action,
562565
{
@@ -567,7 +570,7 @@ describe('mainPrompt', () => {
567570
},
568571
)
569572

570-
expect(toolCalls).toHaveLength(0) // No tool calls expected for empty response
573+
expect(output.type).toBeDefined() // Output should exist even for empty response
571574
})
572575

573576
it('should unescape ampersands in run_terminal_command tool calls', async () => {

backend/src/__tests__/spawn-agents-message-history.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describe('Spawn Agents Message History', () => {
5252
{ role: 'assistant', content: 'Mock agent response' },
5353
],
5454
},
55+
output: { type: 'lastMessage', value: 'Mock agent response' },
5556
}
5657
})
5758
})

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ describe('Spawn Agents Permissions', () => {
7272
{ role: 'assistant', content: 'Mock agent response' },
7373
],
7474
},
75+
output: { type: 'lastMessage', value: 'Mock agent response' },
7576
}
7677
})
7778
})
@@ -327,6 +328,7 @@ describe('Spawn Agents Permissions', () => {
327328
})
328329

329330
const output = await result
331+
console.log('output', output)
330332
expect(JSON.stringify(output)).toContain('Error spawning agent')
331333
expect(JSON.stringify(output)).toContain(
332334
'Agent type nonexistent not found',

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ describe('Subagent Streaming', () => {
8989
{ role: 'assistant', content: 'Test response from subagent' },
9090
],
9191
},
92+
output: { type: 'lastMessage', value: 'Test response from subagent' },
9293
}
9394
})
9495

backend/src/main-prompt.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import { requestToolCall } from './websockets/websocket-action'
1212
import type { AgentTemplate } from './templates/types'
1313
import type { ClientAction } from '@codebuff/common/actions'
1414
import type { CostMode } from '@codebuff/common/constants'
15-
import type { ToolResultPart } from '@codebuff/common/types/messages/content-part'
1615
import type { PrintModeEvent } from '@codebuff/common/types/print-mode'
1716
import type {
1817
SessionState,
1918
AgentTemplateType,
19+
AgentOutput,
2020
} from '@codebuff/common/types/session-state'
2121
import type { WebSocket } from 'ws'
2222

@@ -33,8 +33,7 @@ export const mainPrompt = async (
3333
options: MainPromptOptions,
3434
): Promise<{
3535
sessionState: SessionState
36-
toolCalls: []
37-
toolResults: ToolResultPart[]
36+
output: AgentOutput
3837
}> => {
3938
const { userId, clientSessionId, onResponseChunk, localAgentTemplates } =
4039
options
@@ -102,8 +101,10 @@ export const mainPrompt = async (
102101

103102
return {
104103
sessionState: newSessionState,
105-
toolCalls: [],
106-
toolResults: [],
104+
output: {
105+
type: 'lastMessage',
106+
value: output,
107+
},
107108
}
108109
}
109110
}
@@ -178,7 +179,7 @@ export const mainPrompt = async (
178179
mainAgentTemplate.spawnableAgents = updatedSubagents
179180
localAgentTemplates[agentType] = mainAgentTemplate
180181

181-
const { agentState } = await loopAgentSteps(ws, {
182+
const { agentState, output } = await loopAgentSteps(ws, {
182183
userInputId: promptId,
183184
prompt,
184185
params: promptParams,
@@ -200,7 +201,9 @@ export const mainPrompt = async (
200201
fileContext,
201202
mainAgentState: agentState,
202203
},
203-
toolCalls: [],
204-
toolResults: [],
204+
output: output ?? {
205+
type: 'error' as const,
206+
message: 'No output from agent',
207+
},
205208
}
206209
}

backend/src/run-agent-step.ts

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,20 @@ import { getRequestContext } from './websockets/request-context'
3636
import type { AgentResponseTrace } from '@codebuff/bigquery'
3737
import type { CodebuffToolMessage } from '@codebuff/common/tools/list'
3838
import type { AgentTemplate } from '@codebuff/common/types/agent-template'
39-
import type { Message } from '@codebuff/common/types/messages/codebuff-message'
39+
import type {
40+
AssistantMessage,
41+
Message,
42+
} from '@codebuff/common/types/messages/codebuff-message'
4043
import type { ToolResultPart } from '@codebuff/common/types/messages/content-part'
4144
import type { PrintModeEvent } from '@codebuff/common/types/print-mode'
4245
import type {
4346
AgentTemplateType,
4447
AgentState,
48+
AgentOutput,
4549
} from '@codebuff/common/types/session-state'
4650
import type { ProjectFileContext } from '@codebuff/common/util/file'
4751
import type { WebSocket } from 'ws'
52+
import { getErrorObject } from '@codebuff/common/util/error'
4853

4954
export interface AgentOptions {
5055
userId: string | undefined
@@ -473,7 +478,10 @@ export const loopAgentSteps = async (
473478
clientSessionId: string
474479
onResponseChunk: (chunk: string | PrintModeEvent) => void
475480
},
476-
) => {
481+
): Promise<{
482+
agentState: AgentState
483+
output: AgentOutput
484+
}> => {
477485
const agentTemplate = await getAgentTemplate(agentType, localAgentTemplates)
478486
if (!agentTemplate) {
479487
throw new Error(`Agent template not found for type: ${agentType}`)
@@ -539,7 +547,7 @@ export const loopAgentSteps = async (
539547
},
540548
)
541549

542-
let currentAgentState = {
550+
let currentAgentState: AgentState = {
543551
...agentState,
544552
messageHistory: initialMessages,
545553
}
@@ -584,15 +592,7 @@ export const loopAgentSteps = async (
584592

585593
// End turn if programmatic step ended turn, or if the previous runAgentStep ended turn
586594
if (shouldEndTurn) {
587-
if (clearUserPromptMessagesAfterResponse) {
588-
currentAgentState.messageHistory = expireMessages(
589-
currentAgentState.messageHistory,
590-
'userPrompt',
591-
)
592-
}
593-
return {
594-
agentState: currentAgentState,
595-
}
595+
break
596596
}
597597

598598
const { agentState: newAgentState, shouldEndTurn: llmShouldEndTurn } =
@@ -623,19 +623,67 @@ export const loopAgentSteps = async (
623623
'userPrompt',
624624
)
625625
}
626-
return { agentState: currentAgentState }
626+
627+
return {
628+
agentState: currentAgentState,
629+
output: getAgentOutput(currentAgentState, agentTemplate),
630+
}
627631
} catch (error) {
628-
// Log the error but still return the state with partial costs
629632
logger.error(
630633
{
631-
error,
634+
error: getErrorObject(error),
632635
agentId: currentAgentState.agentId,
633636
creditsUsed: currentAgentState.creditsUsed,
634637
},
635-
'Agent execution failed but returning state with partial costs',
638+
'Agent execution failed',
636639
)
637-
throw error
638-
} finally {
639-
// Ensure costs are always captured, even on failure
640+
const errorObject = getErrorObject(error)
641+
return {
642+
agentState: currentAgentState,
643+
output: {
644+
type: 'error',
645+
message: `${errorObject.name}: ${errorObject.message} ${errorObject.stack ? `\n${errorObject.stack}` : ''}`,
646+
},
647+
}
640648
}
641649
}
650+
651+
function getAgentOutput(
652+
agentState: AgentState,
653+
agentTemplate: AgentTemplate,
654+
): AgentOutput {
655+
if (agentTemplate.outputMode === 'structured_output') {
656+
return {
657+
type: 'structuredOutput',
658+
value: agentState.output ?? null,
659+
}
660+
}
661+
if (agentTemplate.outputMode === 'last_message') {
662+
const assistantMessages = agentState.messageHistory.filter(
663+
(message): message is AssistantMessage => message.role === 'assistant',
664+
)
665+
const lastAssistantMessage = assistantMessages[assistantMessages.length - 1]
666+
if (!lastAssistantMessage) {
667+
return {
668+
type: 'error',
669+
message: 'No response from agent',
670+
}
671+
}
672+
return {
673+
type: 'lastMessage',
674+
value: lastAssistantMessage.content,
675+
}
676+
}
677+
if (agentTemplate.outputMode === 'all_messages') {
678+
// Remove the first message, which includes the previous conversation history.
679+
const agentMessages = agentState.messageHistory.slice(1)
680+
return {
681+
type: 'allMessages',
682+
value: agentMessages,
683+
}
684+
}
685+
agentTemplate.outputMode satisfies never
686+
throw new Error(
687+
`Unknown output mode: ${'outputMode' in agentTemplate ? agentTemplate.outputMode : 'undefined'}`,
688+
)
689+
}

0 commit comments

Comments
 (0)