Skip to content

Commit 3f00123

Browse files
[feat] add parent instructions to custom agents (#214)
Co-authored-by: Codebuff <noreply@codebuff.com>
1 parent 0958532 commit 3f00123

File tree

15 files changed

+620
-35
lines changed

15 files changed

+620
-35
lines changed

.agents/templates/brainstormer.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010
"includeMessageHistory": true,
1111
"toolNames": ["end_turn"],
1212
"spawnableAgents": ["thinker", "researcher"],
13+
"parentInstructions": {
14+
"base": "Spawn brainstormer when you need creative alternatives, want to challenge assumptions, or explore different approaches to implementation problems",
15+
"base_lite": "Use brainstormer for quick creative insights when you're stuck or need fresh perspectives on simple problems",
16+
"base_max": "Leverage brainstormer for deep creative exploration of complex problems with multiple potential solution paths",
17+
"thinker": "Collaborate with brainstormer when analytical thinking needs creative angles or assumption challenging",
18+
"researcher": "Use brainstormer to suggest creative search angles and alternative information sources for research",
19+
"reviewer": "Engage brainstormer for creative problem-solving approaches to code review and innovative improvement suggestions"
20+
},
1321
"promptSchema": {
1422
"prompt": {
1523
"type": "string",
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"id": "knowledge-keeper",
3+
"version": "1.0.0",
4+
"override": false,
5+
"name": "Kendra the Knowledge Keeper",
6+
"purpose": "Expert at gathering, organizing, and maintaining project knowledge files and documentation.",
7+
"model": "anthropic/claude-4-sonnet-20250522",
8+
"outputMode": "last_message",
9+
"includeMessageHistory": false,
10+
"toolNames": [
11+
"read_files",
12+
"write_file",
13+
"code_search",
14+
"web_search",
15+
"read_docs",
16+
"spawn_agents",
17+
"end_turn"
18+
],
19+
"stopSequences": ["</end_turn>"],
20+
"spawnableAgents": ["file_picker", "researcher"],
21+
"promptSchema": {
22+
"prompt": {
23+
"type": "string",
24+
"description": "A request to gather, organize, or update project knowledge"
25+
}
26+
},
27+
"parentInstructions": {
28+
"researcher": "Spawn knowledge-keeper when you find external documentation, API references, or community best practices that contradict or supplement what's currently documented in the project's knowledge files.",
29+
"file_picker": "Spawn knowledge-keeper when you discover configuration files, architectural patterns, or code structures that lack corresponding documentation or when existing knowledge.md files are missing from important directories.",
30+
"reviewer": "Spawn knowledge-keeper when code reviews reveal undocumented design decisions, new patterns being introduced, or when you notice that existing documentation has become outdated due to code changes.",
31+
"thinker": "Spawn knowledge-keeper when your deep analysis uncovers complex architectural trade-offs, system dependencies, or technical debt that should be documented to prevent future confusion.",
32+
"brainstormer": "Spawn knowledge-keeper when you generate innovative solutions for knowledge sharing, discover new ways to organize tribal knowledge, or identify creative approaches to making project information more accessible.",
33+
"base": "Spawn knowledge-keeper when users explicitly ask about project documentation, request explanations of how things work, or when you encounter knowledge gaps while helping with their requests.",
34+
"planner": "Spawn knowledge-keeper when creating long-term documentation strategies, planning knowledge migration between systems, or when developing systematic approaches to capturing institutional memory."
35+
},
36+
"systemPrompt": "You are Kendra the Knowledge Keeper, a specialized agent focused on gathering, organizing, and maintaining project knowledge. Your mission is to ensure that important information about the codebase, patterns, decisions, and institutional memory is properly documented and accessible.\n\nYour core responsibilities:\n1. Knowledge Discovery: Find and analyze existing knowledge files, documentation, and code patterns\n2. Knowledge Organization: Structure information logically and maintain consistency\n3. Knowledge Creation: Create new knowledge files when gaps are identified\n4. Knowledge Maintenance: Update existing knowledge files with new insights\n5. Knowledge Synthesis: Combine information from multiple sources into coherent documentation\n\nAlways start by reading existing knowledge.md files and documentation. Focus on actionable insights that help developers work more effectively. End your response with the end_turn tool.",
37+
"userInputPrompt": "Analyze the current state of project knowledge and provide recommendations for improvements. Focus on knowledge gaps, quality issues, organization problems, and actionable improvements. Then implement the most important changes.",
38+
"agentStepPrompt": "Continue your knowledge management work. Focus on the most impactful improvements and always end with the end_turn tool."
39+
}
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import { AgentState } from '@codebuff/common/types/session-state'
2+
import { ProjectFileContext, FileTreeNode } from '@codebuff/common/util/file'
3+
import { describe, it, expect, beforeEach, afterEach } from 'bun:test'
4+
5+
import { agentRegistry } from '../templates/agent-registry'
6+
import { collectParentInstructions } from '../templates/strings'
7+
import { AgentTemplate } from '../templates/types'
8+
9+
// Helper to create a mock ProjectFileContext
10+
const createMockFileContext = (
11+
agentTemplates: Record<string, string>
12+
): ProjectFileContext => ({
13+
projectRoot: '/test',
14+
cwd: '/test',
15+
knowledgeFiles: {},
16+
userKnowledgeFiles: {},
17+
agentTemplates,
18+
fileTree: [
19+
{
20+
type: 'directory',
21+
name: 'test',
22+
children: [],
23+
filePath: '/test',
24+
} as FileTreeNode,
25+
],
26+
fileTokenScores: {},
27+
gitChanges: {
28+
status: '',
29+
diff: '',
30+
diffCached: '',
31+
lastCommitMessages: '',
32+
},
33+
changesSinceLastChat: {},
34+
shellConfigFiles: {},
35+
systemInfo: {
36+
platform: 'test',
37+
shell: 'test',
38+
nodeVersion: 'test',
39+
arch: 'test',
40+
homedir: '/test',
41+
cpus: 1,
42+
},
43+
})
44+
45+
describe('Parent Instructions Injection', () => {
46+
beforeEach(() => {
47+
agentRegistry.reset()
48+
})
49+
50+
afterEach(() => {
51+
agentRegistry.reset()
52+
})
53+
54+
it('should inject parent instructions into userInputPrompt', async () => {
55+
// Mock file context with agent templates
56+
const fileContext = createMockFileContext({
57+
'.agents/templates/knowledge-keeper.json': JSON.stringify({
58+
version: '1.0.0',
59+
id: 'knowledge-keeper',
60+
name: 'Knowledge Keeper',
61+
purpose: 'Test agent',
62+
model: 'anthropic/claude-4-sonnet-20250522',
63+
outputMode: 'last_message',
64+
includeMessageHistory: false,
65+
toolNames: ['end_turn'],
66+
spawnableAgents: [],
67+
parentInstructions: {
68+
researcher:
69+
'Spawn knowledge-keeper when you find documentation gaps.',
70+
file_picker: 'Spawn knowledge-keeper when you discover missing docs.',
71+
},
72+
systemPrompt: 'You are a test agent.',
73+
userInputPrompt: 'Process the user request.',
74+
agentStepPrompt: 'Continue processing.',
75+
}),
76+
'.agents/templates/researcher.json': JSON.stringify({
77+
version: '1.0.0',
78+
id: 'researcher',
79+
name: 'Researcher',
80+
purpose: 'Research agent',
81+
model: 'anthropic/claude-4-sonnet-20250522',
82+
outputMode: 'last_message',
83+
includeMessageHistory: false,
84+
toolNames: ['end_turn'],
85+
spawnableAgents: [],
86+
systemPrompt: 'You are a researcher.',
87+
userInputPrompt: 'Research the topic.',
88+
agentStepPrompt: 'Continue research.',
89+
}),
90+
})
91+
92+
// Initialize the registry
93+
await agentRegistry.initialize(fileContext)
94+
95+
// Get the researcher template
96+
const researcherTemplate = agentRegistry.getTemplate(
97+
'researcher'
98+
) as AgentTemplate
99+
expect(researcherTemplate).toBeDefined()
100+
101+
// Create mock agent state
102+
const agentState: AgentState = {
103+
agentId: 'test-agent',
104+
agentType: 'researcher',
105+
agentContext: {},
106+
subagents: [],
107+
messageHistory: [],
108+
stepsRemaining: 10,
109+
report: {},
110+
}
111+
112+
// Test parent instructions collection directly
113+
const parentInstructions = await collectParentInstructions(
114+
'researcher',
115+
agentRegistry
116+
)
117+
118+
// Verify that parent instructions are collected
119+
expect(parentInstructions).toHaveLength(1)
120+
expect(parentInstructions[0]).toBe(
121+
'Spawn knowledge-keeper when you find documentation gaps.'
122+
)
123+
})
124+
125+
it('should not inject parent instructions when none exist', async () => {
126+
// Mock file context with agent templates without parentInstructions
127+
const fileContext = createMockFileContext({
128+
'.agents/templates/researcher.json': JSON.stringify({
129+
version: '1.0.0',
130+
id: 'researcher',
131+
name: 'Researcher',
132+
purpose: 'Research agent',
133+
model: 'anthropic/claude-4-sonnet-20250522',
134+
outputMode: 'last_message',
135+
includeMessageHistory: false,
136+
toolNames: ['end_turn'],
137+
spawnableAgents: [],
138+
systemPrompt: 'You are a researcher.',
139+
userInputPrompt: 'Research the topic.',
140+
agentStepPrompt: 'Continue research.',
141+
}),
142+
})
143+
144+
// Initialize the registry
145+
await agentRegistry.initialize(fileContext)
146+
147+
// Get the researcher template
148+
const researcherTemplate = agentRegistry.getTemplate(
149+
'researcher'
150+
) as AgentTemplate
151+
expect(researcherTemplate).toBeDefined()
152+
153+
// Create mock agent state
154+
const agentState: AgentState = {
155+
agentId: 'test-agent',
156+
agentType: 'researcher',
157+
agentContext: {},
158+
subagents: [],
159+
messageHistory: [],
160+
stepsRemaining: 10,
161+
report: {},
162+
}
163+
164+
// Test parent instructions collection directly
165+
const parentInstructions = await collectParentInstructions(
166+
'researcher',
167+
agentRegistry
168+
)
169+
170+
// Verify that no parent instructions are collected
171+
expect(parentInstructions).toHaveLength(0)
172+
})
173+
174+
it('should handle multiple agents with instructions for the same target', async () => {
175+
// Mock file context with multiple agents having instructions for researcher
176+
const fileContext = createMockFileContext({
177+
'.agents/templates/knowledge-keeper.json': JSON.stringify({
178+
version: '1.0.0',
179+
id: 'knowledge-keeper',
180+
name: 'Knowledge Keeper',
181+
purpose: 'Test agent',
182+
model: 'anthropic/claude-4-sonnet-20250522',
183+
outputMode: 'last_message',
184+
includeMessageHistory: false,
185+
toolNames: ['end_turn'],
186+
spawnableAgents: [],
187+
parentInstructions: {
188+
researcher: 'First instruction for researcher.',
189+
},
190+
systemPrompt: 'You are a test agent.',
191+
userInputPrompt: 'Process the user request.',
192+
agentStepPrompt: 'Continue processing.',
193+
}),
194+
'.agents/templates/planner.json': JSON.stringify({
195+
version: '1.0.0',
196+
id: 'planner',
197+
name: 'Planner',
198+
purpose: 'Planning agent',
199+
model: 'anthropic/claude-4-sonnet-20250522',
200+
outputMode: 'last_message',
201+
includeMessageHistory: false,
202+
toolNames: ['end_turn'],
203+
spawnableAgents: [],
204+
parentInstructions: {
205+
researcher: 'Second instruction for researcher.',
206+
},
207+
systemPrompt: 'You are a planner.',
208+
userInputPrompt: 'Plan the task.',
209+
agentStepPrompt: 'Continue planning.',
210+
}),
211+
'.agents/templates/researcher.json': JSON.stringify({
212+
version: '1.0.0',
213+
id: 'researcher',
214+
name: 'Researcher',
215+
purpose: 'Research agent',
216+
model: 'anthropic/claude-4-sonnet-20250522',
217+
outputMode: 'last_message',
218+
includeMessageHistory: false,
219+
toolNames: ['end_turn'],
220+
spawnableAgents: [],
221+
systemPrompt: 'You are a researcher.',
222+
userInputPrompt: 'Research the topic.',
223+
agentStepPrompt: 'Continue research.',
224+
}),
225+
})
226+
227+
// Initialize the registry
228+
await agentRegistry.initialize(fileContext)
229+
230+
// Get the researcher template
231+
const researcherTemplate = agentRegistry.getTemplate(
232+
'researcher'
233+
) as AgentTemplate
234+
expect(researcherTemplate).toBeDefined()
235+
236+
// Create mock agent state
237+
const agentState: AgentState = {
238+
agentId: 'test-agent',
239+
agentType: 'researcher',
240+
agentContext: {},
241+
subagents: [],
242+
messageHistory: [],
243+
stepsRemaining: 10,
244+
report: {},
245+
}
246+
247+
// Test parent instructions collection directly
248+
const parentInstructions = await collectParentInstructions(
249+
'researcher',
250+
agentRegistry
251+
)
252+
253+
// Verify that both parent instructions are collected
254+
expect(parentInstructions).toHaveLength(2)
255+
expect(parentInstructions).toContain('First instruction for researcher.')
256+
expect(parentInstructions).toContain('Second instruction for researcher.')
257+
})
258+
})

backend/src/templates/agents/ask.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const ask = (model: Model): Omit<AgentTemplate, 'id'> => ({
3838
stepAssistantPrefix: '',
3939

4040
systemPrompt:
41-
`# Persona: ${PLACEHOLDER.AGENT_NAME} - The Enthusiastic Coding Assistant
41+
`# Persona: ${PLACEHOLDER.AGENT_NAME}
4242
4343
` + askAgentSystemPrompt(model),
4444
userInputPrompt: askAgentUserInputPrompt(model),

backend/src/templates/agents/base.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,7 @@ export const base = (
5151
stepAssistantMessage: '',
5252
stepAssistantPrefix: '',
5353

54-
systemPrompt:
55-
`# Persona: ${PLACEHOLDER.AGENT_NAME} - The Enthusiastic Coding Assistant
56-
57-
` + baseAgentSystemPrompt(model),
54+
systemPrompt: baseAgentSystemPrompt(model),
5855
userInputPrompt: baseAgentUserInputPrompt(model),
5956
agentStepPrompt: baseAgentAgentStepPrompt(model),
6057
})

backend/src/templates/base-prompts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { closeXml } from '@codebuff/common/util/xml'
66
import { PLACEHOLDER } from './types'
77

88
export const baseAgentSystemPrompt = (model: Model) => {
9-
return `# Persona: ${PLACEHOLDER.AGENT_NAME} - The Enthusiastic Coding Assistant
9+
return `# Persona: ${PLACEHOLDER.AGENT_NAME}
1010
11-
**Your core identity is ${PLACEHOLDER.AGENT_NAME}.** ${PLACEHOLDER.AGENT_NAME} is an expert coding assistant who is enthusiastic, proactive, and helpful.
11+
**Your core identity is ${PLACEHOLDER.AGENT_NAME}.** You are an expert coding assistant who is enthusiastic, proactive, and helpful.
1212
1313
- **Tone:** Maintain a positive, friendly, and helpful tone. Use clear and encouraging language.
1414
- **Clarity & Conciseness:** Explain your steps clearly but concisely. Say the least you can to get your point across. If you can, answer in one sentence only. Do not summarize changes. End turn early.

backend/src/templates/dynamic-agent-service.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { DynamicAgentTemplateSchema } from '@codebuff/common/types/dynamic-agent
44
import { AgentTemplateType } from '@codebuff/common/types/session-state'
55
import { normalizeAgentNames } from '@codebuff/common/util/agent-name-normalization'
66
import {
7+
formatParentInstructionsError,
78
formatSpawnableAgentError,
9+
validateParentInstructions,
810
validateSpawnableAgents,
911
} from '@codebuff/common/util/agent-template-validation'
1012
import { ProjectFileContext } from '@codebuff/common/util/file'
@@ -196,6 +198,25 @@ export class DynamicAgentService {
196198
return
197199
}
198200

201+
// Validate parent instructions if they exist
202+
if (dynamicAgent.parentInstructions) {
203+
const parentInstructionsValidation = validateParentInstructions(
204+
dynamicAgent.parentInstructions,
205+
dynamicAgentIds
206+
)
207+
if (!parentInstructionsValidation.valid) {
208+
this.validationErrors.push({
209+
filePath,
210+
message: formatParentInstructionsError(
211+
parentInstructionsValidation.invalidAgents,
212+
parentInstructionsValidation.availableAgents
213+
),
214+
details: `Available agents: ${parentInstructionsValidation.availableAgents.join(', ')}`,
215+
})
216+
return
217+
}
218+
}
219+
199220
const validatedSpawnableAgents = normalizeAgentNames(
200221
dynamicAgent.spawnableAgents
201222
) as AgentTemplateType[]
@@ -232,6 +253,7 @@ export class DynamicAgentService {
232253
includeMessageHistory: dynamicAgent.includeMessageHistory,
233254
toolNames: dynamicAgent.toolNames as any[],
234255
spawnableAgents: validatedSpawnableAgents,
256+
parentInstructions: dynamicAgent.parentInstructions,
235257

236258
systemPrompt: this.resolvePromptFieldFromAgentTemplates(
237259
dynamicAgent.systemPrompt,

0 commit comments

Comments
 (0)