Skip to content

Commit b30e2ef

Browse files
committed
Put back subagents in AgentState
1 parent 58db758 commit b30e2ef

File tree

7 files changed

+211
-8
lines changed

7 files changed

+211
-8
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('QuickJS Sandbox Generator', () => {
2525
messageHistory: [],
2626
output: undefined,
2727
agentContext: {},
28-
spawnableAgents: [],
28+
subagents: [],
2929
stepsRemaining: 10,
3030
}
3131

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export const handleSpawnAgentInline = ((params: {
133133
agentId,
134134
agentType,
135135
agentContext: agentState!.agentContext, // Inherit parent context directly
136-
spawnableAgents: [],
136+
subagents: [],
137137
messageHistory: getLatestState().messages, // Share the same message array
138138
stepsRemaining: 20, // MAX_AGENT_STEPS
139139
output: undefined,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export const handleSpawnAgentsAsync = ((params: {
182182
agentId,
183183
agentType,
184184
agentContext: {},
185-
spawnableAgents: [],
185+
subagents: [],
186186
messageHistory: subAgentMessages,
187187
stepsRemaining: 20, // MAX_AGENT_STEPS
188188
output: undefined,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export const handleSpawnAgents = ((params: {
161161
agentId,
162162
agentType,
163163
agentContext: {},
164-
spawnableAgents: [],
164+
subagents: [],
165165
messageHistory: subAgentMessages,
166166
stepsRemaining: 20, // MAX_AGENT_STEPS
167167
output: undefined,
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { generateCompactId } from '@codebuff/common/util/string'
2+
3+
import { getAgentTemplate } from '../../../templates/agent-registry'
4+
import { logger } from '../../../util/logger'
5+
import { expireMessages } from '../../../util/messages'
6+
7+
import type { CodebuffToolCall } from '../../constants'
8+
import type { CodebuffToolHandlerFunction } from '../handler-function-type'
9+
import type { AgentTemplate } from '@codebuff/common/types/agent-template'
10+
import type { CodebuffMessage } from '@codebuff/common/types/message'
11+
import type { PrintModeEvent } from '@codebuff/common/types/print-mode'
12+
import type {
13+
AgentState,
14+
AgentTemplateType,
15+
} from '@codebuff/common/types/session-state'
16+
import type { ProjectFileContext } from '@codebuff/common/util/file'
17+
import type { WebSocket } from 'ws'
18+
19+
export const handleSpawnAgentInline = ((params: {
20+
previousToolCallFinished: Promise<void>
21+
toolCall: CodebuffToolCall<'spawn_agent_inline'>
22+
23+
fileContext: ProjectFileContext
24+
clientSessionId: string
25+
userInputId: string
26+
27+
getLatestState: () => { messages: CodebuffMessage[] }
28+
state: {
29+
ws?: WebSocket
30+
fingerprintId?: string
31+
userId?: string
32+
agentTemplate?: AgentTemplate
33+
localAgentTemplates?: Record<string, AgentTemplate>
34+
messages?: CodebuffMessage[]
35+
agentState?: AgentState
36+
}
37+
}): { result: Promise<undefined>; state: {} } => {
38+
const {
39+
previousToolCallFinished,
40+
toolCall,
41+
fileContext,
42+
clientSessionId,
43+
userInputId,
44+
getLatestState,
45+
state,
46+
} = params
47+
const {
48+
agent_type: agentTypeStr,
49+
prompt,
50+
params: agentParams,
51+
} = toolCall.args
52+
const {
53+
ws,
54+
fingerprintId,
55+
userId,
56+
agentTemplate: parentAgentTemplate,
57+
localAgentTemplates,
58+
messages,
59+
} = state
60+
let { agentState } = state
61+
62+
if (!ws) {
63+
throw new Error(
64+
'Internal error for spawn_agent_inline: Missing WebSocket in state',
65+
)
66+
}
67+
if (!fingerprintId) {
68+
throw new Error(
69+
'Internal error for spawn_agent_inline: Missing fingerprintId in state',
70+
)
71+
}
72+
if (!parentAgentTemplate) {
73+
throw new Error(
74+
'Internal error for spawn_agent_inline: Missing agentTemplate in state',
75+
)
76+
}
77+
if (!messages) {
78+
throw new Error(
79+
'Internal error for spawn_agent_inline: Missing messages in state',
80+
)
81+
}
82+
if (!agentState) {
83+
throw new Error(
84+
'Internal error for spawn_agent_inline: Missing agentState in state',
85+
)
86+
}
87+
if (!localAgentTemplates) {
88+
throw new Error(
89+
'Internal error for spawn_agent_inline: Missing localAgentTemplates in state',
90+
)
91+
}
92+
93+
const triggerSpawnInlineAgent = async () => {
94+
const agentType = agentTypeStr as AgentTemplateType
95+
const agentTemplate = await getAgentTemplate(agentType, localAgentTemplates)
96+
97+
if (!agentTemplate) {
98+
throw new Error(`Agent type ${agentTypeStr} not found.`)
99+
}
100+
101+
if (!parentAgentTemplate.spawnableAgents.includes(agentType)) {
102+
throw new Error(
103+
`Agent type ${parentAgentTemplate.id} is not allowed to spawn child agent type ${agentType}.`,
104+
)
105+
}
106+
107+
// Validate prompt and params against agent's schema
108+
const { inputSchema } = agentTemplate
109+
110+
// Validate prompt requirement
111+
if (inputSchema.prompt) {
112+
const result = inputSchema.prompt.safeParse(prompt)
113+
if (!result.success) {
114+
throw new Error(
115+
`Invalid prompt for agent ${agentType}: ${JSON.stringify(result.error.issues, null, 2)}`,
116+
)
117+
}
118+
}
119+
120+
// Validate params if schema exists
121+
if (inputSchema.params) {
122+
const result = inputSchema.params.safeParse(agentParams)
123+
if (!result.success) {
124+
throw new Error(
125+
`Invalid params for agent ${agentType}: ${JSON.stringify(result.error.issues, null, 2)}`,
126+
)
127+
}
128+
}
129+
130+
const agentId = generateCompactId()
131+
132+
// Create child agent state that shares message history with parent
133+
const childAgentState: AgentState = {
134+
agentId,
135+
agentType,
136+
agentContext: agentState!.agentContext, // Inherit parent context directly
137+
subagents: [],
138+
messageHistory: getLatestState().messages, // Share the same message array
139+
stepsRemaining: 20, // MAX_AGENT_STEPS
140+
output: undefined,
141+
parentId: agentState!.agentId,
142+
}
143+
144+
logger.debug(
145+
{
146+
agentTemplate,
147+
prompt,
148+
params: agentParams,
149+
agentId,
150+
parentId: childAgentState.parentId,
151+
},
152+
`Spawning inline agent — ${agentType} (${agentId})`,
153+
)
154+
155+
// Import loopAgentSteps dynamically to avoid circular dependency
156+
const { loopAgentSteps } = await import('../../../run-agent-step')
157+
158+
const result = await loopAgentSteps(ws, {
159+
userInputId: `${userInputId}-inline-${agentType}${agentId}`,
160+
prompt: prompt || '',
161+
params: agentParams,
162+
agentType: agentTemplate.id,
163+
agentState: childAgentState,
164+
fingerprintId,
165+
fileContext,
166+
localAgentTemplates,
167+
toolResults: [],
168+
userId,
169+
clientSessionId,
170+
onResponseChunk: (chunk: string | PrintModeEvent) => {
171+
// Child agent output is streamed directly to parent's output
172+
// No need for special handling since we share message history
173+
},
174+
})
175+
176+
// Update parent's message history with child's final state
177+
// Since we share the same message array reference, this should already be updated
178+
let finalMessages = result.agentState?.messageHistory || state.messages
179+
180+
// Expire messages with timeToLive: 'userPrompt' to clean up inline agent's temporary messages
181+
finalMessages = expireMessages(finalMessages, 'userPrompt')
182+
183+
state.messages = finalMessages
184+
185+
// Update parent agent state to reflect shared message history
186+
if (agentState && result.agentState) {
187+
agentState.messageHistory = finalMessages
188+
}
189+
190+
return undefined
191+
}
192+
193+
return {
194+
result: previousToolCallFinished.then(triggerSpawnInlineAgent),
195+
state: {},
196+
}
197+
}) satisfies CodebuffToolHandlerFunction<'spawn_agent_inline'>

common/src/types/session-state.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const AgentStateSchema: z.ZodType<{
3434
agentId: string
3535
agentType: AgentTemplateType | null
3636
agentContext: Record<string, Subgoal>
37-
spawnableAgents: AgentState[]
37+
subagents: AgentState[]
3838
messageHistory: CodebuffMessage[]
3939
stepsRemaining: number
4040
output?: Record<string, any>
@@ -44,7 +44,7 @@ export const AgentStateSchema: z.ZodType<{
4444
agentId: z.string(),
4545
agentType: z.string().nullable(),
4646
agentContext: z.record(z.string(), subgoalSchema),
47-
spawnableAgents: AgentStateSchema.array(),
47+
subagents: AgentStateSchema.array(),
4848
messageHistory: CodebuffMessageSchema.array(),
4949
stepsRemaining: z.number(),
5050
output: z.record(z.string(), z.any()).optional(),
@@ -105,7 +105,7 @@ export function getInitialSessionState(
105105
agentId: 'main-agent',
106106
agentType: null,
107107
agentContext: {},
108-
spawnableAgents: [],
108+
subagents: [],
109109
messageHistory: [],
110110
stepsRemaining: 12,
111111
output: undefined,

common/src/util/types/agent-config.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ export interface AgentConfig {
4141
/** Tools this agent can use. */
4242
toolNames?: ToolName[]
4343

44-
/** Other agents this agent can spawn. */
44+
/** Other agents this agent can spawn, like 'codebuff/file-picker@0.0.1'.
45+
*
46+
* Use the fully qualified agent id from the agent store, including publisher and version: 'codebuff/file-picker@0.0.1'
47+
* (publisher and version are required!)
48+
*
49+
* Or, use the agent id from a local agent file in your .agents directory: 'file-picker'.
50+
*/
4551
spawnableAgents?: string[]
4652

4753
// ============================================================================

0 commit comments

Comments
 (0)