Skip to content

Commit c78660d

Browse files
committed
fix: added spawnerPrompt to parent instruction
1 parent e1d1199 commit c78660d

File tree

2 files changed

+310
-2
lines changed

2 files changed

+310
-2
lines changed
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
import { TEST_AGENT_RUNTIME_IMPL } from '@codebuff/common/testing/impl/agent-runtime'
2+
import { describe, test, expect, mock } from 'bun:test'
3+
import { z } from 'zod/v4'
4+
5+
import { getAgentPrompt } from '../strings'
6+
7+
import type { AgentTemplate } from '../types'
8+
import type { AgentState } from '@codebuff/common/types/session-state'
9+
import type { ProjectFileContext } from '@codebuff/common/util/file'
10+
11+
/** Create a mock logger using bun:test mock() for better test consistency */
12+
const createMockLogger = () => ({
13+
debug: mock(() => {}),
14+
info: mock(() => {}),
15+
warn: mock(() => {}),
16+
error: mock(() => {}),
17+
})
18+
19+
const createMockFileContext = (): ProjectFileContext => ({
20+
projectRoot: '/test',
21+
cwd: '/test',
22+
fileTree: [],
23+
fileTokenScores: {},
24+
knowledgeFiles: {},
25+
gitChanges: {
26+
status: '',
27+
diff: '',
28+
diffCached: '',
29+
lastCommitMessages: '',
30+
},
31+
changesSinceLastChat: {},
32+
shellConfigFiles: {},
33+
agentTemplates: {},
34+
customToolDefinitions: {},
35+
systemInfo: {
36+
platform: 'test',
37+
shell: 'test',
38+
nodeVersion: 'test',
39+
arch: 'test',
40+
homedir: '/home/test',
41+
cpus: 1,
42+
},
43+
})
44+
45+
const createMockAgentState = (agentType: string): AgentState => ({
46+
agentId: 'test-agent-id',
47+
agentType,
48+
runId: 'test-run-id',
49+
parentId: undefined,
50+
messageHistory: [],
51+
output: undefined,
52+
stepsRemaining: 10,
53+
creditsUsed: 0,
54+
directCreditsUsed: 0,
55+
childRunIds: [],
56+
ancestorRunIds: [],
57+
contextTokenCount: 0,
58+
agentContext: {},
59+
})
60+
61+
const createMockAgentTemplate = (
62+
overrides: Partial<AgentTemplate> = {},
63+
): AgentTemplate => ({
64+
id: 'test-agent',
65+
displayName: 'Test Agent',
66+
model: 'gpt-4o-mini',
67+
inputSchema: {},
68+
outputMode: 'last_message',
69+
includeMessageHistory: false,
70+
inheritParentSystemPrompt: false,
71+
mcpServers: {},
72+
toolNames: [],
73+
spawnableAgents: [],
74+
systemPrompt: '',
75+
instructionsPrompt: 'Test instructions',
76+
stepPrompt: '',
77+
...overrides,
78+
})
79+
80+
describe('getAgentPrompt', () => {
81+
describe('spawnerPrompt inclusion in instructionsPrompt', () => {
82+
test('includes spawnerPrompt for each spawnable agent with spawnerPrompt defined', async () => {
83+
const filePickerTemplate = createMockAgentTemplate({
84+
id: 'file-picker',
85+
displayName: 'File Picker',
86+
spawnerPrompt: 'Spawn to find relevant files in a codebase',
87+
})
88+
89+
const codeSearcherTemplate = createMockAgentTemplate({
90+
id: 'code-searcher',
91+
displayName: 'Code Searcher',
92+
spawnerPrompt: 'Mechanically runs multiple code search queries',
93+
})
94+
95+
const mainAgentTemplate = createMockAgentTemplate({
96+
id: 'main-agent',
97+
displayName: 'Main Agent',
98+
spawnableAgents: ['file-picker', 'code-searcher'],
99+
instructionsPrompt: 'Main agent instructions.',
100+
})
101+
102+
const agentTemplates: Record<string, AgentTemplate> = {
103+
'main-agent': mainAgentTemplate,
104+
'file-picker': filePickerTemplate,
105+
'code-searcher': codeSearcherTemplate,
106+
}
107+
108+
const result = await getAgentPrompt({
109+
agentTemplate: mainAgentTemplate,
110+
promptType: { type: 'instructionsPrompt' },
111+
fileContext: createMockFileContext(),
112+
agentState: createMockAgentState('main-agent'),
113+
agentTemplates,
114+
additionalToolDefinitions: async () => ({}),
115+
logger: createMockLogger(),
116+
apiKey: TEST_AGENT_RUNTIME_IMPL.apiKey,
117+
databaseAgentCache: TEST_AGENT_RUNTIME_IMPL.databaseAgentCache,
118+
fetchAgentFromDatabase: TEST_AGENT_RUNTIME_IMPL.fetchAgentFromDatabase,
119+
})
120+
121+
expect(result).toBeDefined()
122+
expect(result).toContain('You can spawn the following agents:')
123+
expect(result).toContain('- file-picker: Spawn to find relevant files in a codebase')
124+
expect(result).toContain('- code-searcher: Mechanically runs multiple code search queries')
125+
})
126+
127+
test('includes only agent name when spawnerPrompt is not defined', async () => {
128+
const agentWithoutSpawnerPrompt = createMockAgentTemplate({
129+
id: 'no-prompt-agent',
130+
displayName: 'No Prompt Agent',
131+
// spawnerPrompt is not defined
132+
})
133+
134+
const mainAgentTemplate = createMockAgentTemplate({
135+
id: 'main-agent',
136+
displayName: 'Main Agent',
137+
spawnableAgents: ['no-prompt-agent'],
138+
instructionsPrompt: 'Main agent instructions.',
139+
})
140+
141+
const agentTemplates: Record<string, AgentTemplate> = {
142+
'main-agent': mainAgentTemplate,
143+
'no-prompt-agent': agentWithoutSpawnerPrompt,
144+
}
145+
146+
const result = await getAgentPrompt({
147+
agentTemplate: mainAgentTemplate,
148+
promptType: { type: 'instructionsPrompt' },
149+
fileContext: createMockFileContext(),
150+
agentState: createMockAgentState('main-agent'),
151+
agentTemplates,
152+
additionalToolDefinitions: async () => ({}),
153+
logger: createMockLogger(),
154+
apiKey: TEST_AGENT_RUNTIME_IMPL.apiKey,
155+
databaseAgentCache: TEST_AGENT_RUNTIME_IMPL.databaseAgentCache,
156+
fetchAgentFromDatabase: TEST_AGENT_RUNTIME_IMPL.fetchAgentFromDatabase,
157+
})
158+
159+
expect(result).toBeDefined()
160+
expect(result).toContain('You can spawn the following agents:')
161+
expect(result).toContain('- no-prompt-agent')
162+
// Should not have a colon after the agent name when there's no spawnerPrompt
163+
expect(result).not.toContain('- no-prompt-agent:')
164+
})
165+
166+
test('handles mix of agents with and without spawnerPrompt', async () => {
167+
const agentWithPrompt = createMockAgentTemplate({
168+
id: 'with-prompt',
169+
displayName: 'Agent With Prompt',
170+
spawnerPrompt: 'This agent has a description',
171+
})
172+
173+
const agentWithoutPrompt = createMockAgentTemplate({
174+
id: 'without-prompt',
175+
displayName: 'Agent Without Prompt',
176+
// spawnerPrompt is not defined
177+
})
178+
179+
const mainAgentTemplate = createMockAgentTemplate({
180+
id: 'main-agent',
181+
displayName: 'Main Agent',
182+
spawnableAgents: ['with-prompt', 'without-prompt'],
183+
instructionsPrompt: 'Main agent instructions.',
184+
})
185+
186+
const agentTemplates: Record<string, AgentTemplate> = {
187+
'main-agent': mainAgentTemplate,
188+
'with-prompt': agentWithPrompt,
189+
'without-prompt': agentWithoutPrompt,
190+
}
191+
192+
const result = await getAgentPrompt({
193+
agentTemplate: mainAgentTemplate,
194+
promptType: { type: 'instructionsPrompt' },
195+
fileContext: createMockFileContext(),
196+
agentState: createMockAgentState('main-agent'),
197+
agentTemplates,
198+
additionalToolDefinitions: async () => ({}),
199+
logger: createMockLogger(),
200+
apiKey: TEST_AGENT_RUNTIME_IMPL.apiKey,
201+
databaseAgentCache: TEST_AGENT_RUNTIME_IMPL.databaseAgentCache,
202+
fetchAgentFromDatabase: TEST_AGENT_RUNTIME_IMPL.fetchAgentFromDatabase,
203+
})
204+
205+
expect(result).toBeDefined()
206+
expect(result).toContain('- with-prompt: This agent has a description')
207+
expect(result).toContain('- without-prompt')
208+
expect(result).not.toContain('- without-prompt:')
209+
})
210+
211+
test('does not include spawnable agents section when no spawnable agents defined', async () => {
212+
const mainAgentTemplate = createMockAgentTemplate({
213+
id: 'main-agent',
214+
displayName: 'Main Agent',
215+
spawnableAgents: [],
216+
instructionsPrompt: 'Main agent instructions.',
217+
})
218+
219+
const agentTemplates: Record<string, AgentTemplate> = {
220+
'main-agent': mainAgentTemplate,
221+
}
222+
223+
const result = await getAgentPrompt({
224+
agentTemplate: mainAgentTemplate,
225+
promptType: { type: 'instructionsPrompt' },
226+
fileContext: createMockFileContext(),
227+
agentState: createMockAgentState('main-agent'),
228+
agentTemplates,
229+
additionalToolDefinitions: async () => ({}),
230+
logger: createMockLogger(),
231+
apiKey: TEST_AGENT_RUNTIME_IMPL.apiKey,
232+
databaseAgentCache: TEST_AGENT_RUNTIME_IMPL.databaseAgentCache,
233+
fetchAgentFromDatabase: TEST_AGENT_RUNTIME_IMPL.fetchAgentFromDatabase,
234+
})
235+
236+
expect(result).toBeDefined()
237+
expect(result).not.toContain('You can spawn the following agents:')
238+
})
239+
240+
test('does not include spawnable agents for non-instructionsPrompt types', async () => {
241+
const filePickerTemplate = createMockAgentTemplate({
242+
id: 'file-picker',
243+
displayName: 'File Picker',
244+
spawnerPrompt: 'Spawn to find relevant files in a codebase',
245+
})
246+
247+
const mainAgentTemplate = createMockAgentTemplate({
248+
id: 'main-agent',
249+
displayName: 'Main Agent',
250+
spawnableAgents: ['file-picker'],
251+
systemPrompt: 'System prompt content.',
252+
stepPrompt: 'Step prompt content.',
253+
})
254+
255+
const agentTemplates: Record<string, AgentTemplate> = {
256+
'main-agent': mainAgentTemplate,
257+
'file-picker': filePickerTemplate,
258+
}
259+
260+
// Test systemPrompt - should not include spawnable agents
261+
const systemResult = await getAgentPrompt({
262+
agentTemplate: mainAgentTemplate,
263+
promptType: { type: 'systemPrompt' },
264+
fileContext: createMockFileContext(),
265+
agentState: createMockAgentState('main-agent'),
266+
agentTemplates,
267+
additionalToolDefinitions: async () => ({}),
268+
logger: createMockLogger(),
269+
apiKey: TEST_AGENT_RUNTIME_IMPL.apiKey,
270+
databaseAgentCache: TEST_AGENT_RUNTIME_IMPL.databaseAgentCache,
271+
fetchAgentFromDatabase: TEST_AGENT_RUNTIME_IMPL.fetchAgentFromDatabase,
272+
})
273+
274+
expect(systemResult).toBeDefined()
275+
expect(systemResult).not.toContain('You can spawn the following agents:')
276+
277+
// Test stepPrompt - should not include spawnable agents
278+
const stepResult = await getAgentPrompt({
279+
agentTemplate: mainAgentTemplate,
280+
promptType: { type: 'stepPrompt' },
281+
fileContext: createMockFileContext(),
282+
agentState: createMockAgentState('main-agent'),
283+
agentTemplates,
284+
additionalToolDefinitions: async () => ({}),
285+
logger: createMockLogger(),
286+
apiKey: TEST_AGENT_RUNTIME_IMPL.apiKey,
287+
databaseAgentCache: TEST_AGENT_RUNTIME_IMPL.databaseAgentCache,
288+
fetchAgentFromDatabase: TEST_AGENT_RUNTIME_IMPL.fetchAgentFromDatabase,
289+
})
290+
291+
expect(stepResult).toBeDefined()
292+
expect(stepResult).not.toContain('You can spawn the following agents:')
293+
})
294+
})
295+
})

packages/agent-runtime/src/templates/strings.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,21 @@ export async function getAgentPrompt<T extends StringField>(
202202
}
203203
} else if (spawnableAgents.length > 0) {
204204
// For non-inherited tools, agents are already defined as tools with full schemas,
205-
// so we just list the available agent IDs here
206-
addendum += `\n\nYou can spawn the following agents: ${spawnableAgents.join(', ')}.`
205+
// so we add the spawnerPrompt for each agent
206+
const agentDescriptions = await Promise.all(
207+
spawnableAgents.map(async (agentType) => {
208+
const template = await getAgentTemplate({
209+
...params,
210+
agentId: agentType,
211+
localAgentTemplates: agentTemplates,
212+
})
213+
if (template?.spawnerPrompt) {
214+
return `- ${agentType}: ${template.spawnerPrompt}`
215+
}
216+
return `- ${agentType}`
217+
}),
218+
)
219+
addendum += `\n\nYou can spawn the following agents:\n\n${agentDescriptions.join('\n')}`
207220
}
208221

209222
// Add output schema information if defined

0 commit comments

Comments
 (0)