Skip to content

Commit 45bdab1

Browse files
authored
Merge pull request #84 from phantom5099/plan-subagent
add plan subagent
2 parents 81f30ad + d6f9997 commit 45bdab1

9 files changed

Lines changed: 152 additions & 12 deletions

File tree

‎packages/codingcode/src/agent/prompt.ts‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,45 @@ export function buildSystemPrompt(opts: SystemPromptOptions): string {
7373
const enabledProfiles = opts.agentProfiles.filter((p) => !p.disabled);
7474
if (enabledProfiles.length > 0) {
7575
prompt += '\n\n## Available Subagents\n';
76-
prompt += `You can dispatch subagents using the dispatch_agent tool. Available profiles:\n`;
76+
prompt += 'You can dispatch subagents using the dispatch_agent tool. Available profiles:\n';
7777
for (const p of enabledProfiles) {
7878
prompt += `\n### ${p.name}\n${p.description}`;
7979
if (p.tools && p.tools.length > 0) {
8080
prompt += `\nTools: ${p.tools.join(', ')}`;
8181
}
8282
}
83+
84+
prompt += `
85+
86+
### When to dispatch
87+
88+
Dispatch a subagent when the task involves extensively reading files, searching across the codebase, or analyzing a whole module. A subagent runs in an independent context window — all of its tool calls (read_file, search_code, etc.) consume only the subagent\'s own context. Only the final result comes back to you.
89+
90+
**Dispatch = protect your context window.** If you do the same work yourself, all raw content goes directly into your context.
91+
92+
### When NOT to dispatch
93+
94+
- The task needs only a small amount of information — do it yourself.
95+
- You already know the exact file path and what to look for — use read_file / search_code directly.
96+
97+
### Rules
98+
99+
1. Once you dispatch a subagent, do **NOT** also perform the same searches yourself.
100+
2. **Do NOT peek** — the subagent runs independently. Do not try to read its intermediate output, as that defeats the context protection.
101+
3. When the subagent returns, relay its conclusion to the user concisely.
102+
103+
### Example
104+
105+
\`\`\`
106+
User: "Find all API route definitions in this project."
107+
108+
Thinking: This requires searching multiple directories broadly. If I grep and read files myself, all the raw output piles into my context. I should dispatch explore.
109+
110+
dispatch_agent({
111+
agent: "explore",
112+
prompt: "Search the entire project for API route definitions..."
113+
})
114+
\`\`\``;
83115
}
84116
}
85117

‎packages/codingcode/src/client/direct/settings.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
} from '../../subagent/loader.js';
2323
import {
2424
EXPLORE_PROFILE,
25+
PLAN_PROFILE,
2526
setSubagentEnabledState,
2627
resolveSubagentEnabled,
2728
getProjectSubagentEnabledState,
@@ -132,7 +133,7 @@ function agentsList(cwd: string): Array<{
132133
projectDisabled?: boolean;
133134
}> {
134135
const custom = loadAgentProfiles(cwd);
135-
return [EXPLORE_PROFILE, ...custom].map((a) => {
136+
return [EXPLORE_PROFILE, PLAN_PROFILE, ...custom].map((a) => {
136137
const projectVal = getProjectAgentDisabledState(cwd, a.name);
137138
return {
138139
name: a.name,
@@ -143,7 +144,7 @@ function agentsList(cwd: string): Array<{
143144
maxSteps: a.maxSteps,
144145
model: a.model,
145146
disabled: resolveAgentDisabled(cwd, a.name),
146-
source: a.name === EXPLORE_PROFILE.name ? ('builtin' as const) : ('project' as const),
147+
source: a.name === EXPLORE_PROFILE.name || a.name === PLAN_PROFILE.name ? ('builtin' as const) : ('project' as const),
147148
hasProjectOverride: projectVal !== undefined,
148149
projectDisabled: projectVal,
149150
};

‎packages/codingcode/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export type { SystemPromptVariant, SystemPromptOptions } from './agent/prompt.js
6565
export {
6666
SubagentRegistry,
6767
EXPLORE_PROFILE,
68+
PLAN_PROFILE,
6869
getSubagentEnabledState,
6970
setSubagentEnabledState,
7071
} from './subagent/registry.js';

‎packages/codingcode/src/runtime/project-runtime.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Effect } from 'effect';
22
import type { AgentProfile } from '../subagent/registry';
3-
import { EXPLORE_PROFILE } from '../subagent/registry';
3+
import { EXPLORE_PROFILE, PLAN_PROFILE } from '../subagent/registry';
44
import * as agentLoader from '../subagent/loader';
55
import type { ToolVisibilityPolicy } from '../tools/types';
66
import { HookService } from '../hooks/registry';
@@ -22,6 +22,7 @@ export class ProjectRuntimeService extends Effect.Service<ProjectRuntimeService>
2222
function buildProfiles(projectPath: string): AgentProfile[] {
2323
const profiles: AgentProfile[] = [];
2424
profiles.push(EXPLORE_PROFILE);
25+
profiles.push(PLAN_PROFILE);
2526

2627
for (const p of agentLoader.loadGlobalAgentProfiles()) {
2728
if (!profiles.find((existing) => existing.name === p.name)) {

‎packages/codingcode/src/server/routes/settings.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
} from '../../subagent/loader.js';
3131
import {
3232
EXPLORE_PROFILE,
33+
PLAN_PROFILE,
3334
resolveSubagentEnabled,
3435
getProjectSubagentEnabledState,
3536
setProjectSubagentEnabledState,
@@ -155,6 +156,22 @@ function agentsList(cwd: string): Array<{
155156
projectDisabled: exploreProjectVal,
156157
});
157158

159+
// builtin: PLAN_PROFILE
160+
const planProjectVal = getProjectAgentDisabledState(cwd, PLAN_PROFILE.name);
161+
result.push({
162+
name: PLAN_PROFILE.name,
163+
description: PLAN_PROFILE.description,
164+
tools: PLAN_PROFILE.tools,
165+
mcpServers: PLAN_PROFILE.mcpServers,
166+
readonly: PLAN_PROFILE.readonly,
167+
maxSteps: PLAN_PROFILE.maxSteps,
168+
model: PLAN_PROFILE.model,
169+
disabled: resolveAgentDisabled(cwd, PLAN_PROFILE.name),
170+
source: 'builtin',
171+
hasProjectOverride: planProjectVal !== undefined,
172+
projectDisabled: planProjectVal,
173+
});
174+
158175
// global agents (not overridden by project)
159176
for (const a of globalCustom) {
160177
if (projectNames.has(a.name)) continue;
@@ -296,7 +313,7 @@ settingsRouter.get('/agents', (c) => {
296313
if (isGlobalCwd(rawCwd)) {
297314
const custom = loadGlobalAgentProfiles();
298315
return c.json(
299-
[EXPLORE_PROFILE, ...custom].map((a) => ({
316+
[EXPLORE_PROFILE, PLAN_PROFILE, ...custom].map((a) => ({
300317
name: a.name,
301318
description: a.description,
302319
tools: a.tools,
@@ -305,7 +322,7 @@ settingsRouter.get('/agents', (c) => {
305322
maxSteps: a.maxSteps,
306323
model: a.model,
307324
disabled: getGlobalAgentDisabledState(a.name),
308-
source: a.name === EXPLORE_PROFILE.name ? 'builtin' : 'global',
325+
source: a.name === EXPLORE_PROFILE.name || a.name === PLAN_PROFILE.name ? 'builtin' : 'global',
309326
}))
310327
);
311328
}

‎packages/codingcode/src/subagent/registry.ts‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,5 +211,31 @@ export const EXPLORE_PROFILE: AgentProfile = {
211211
'You are a read-only code exploration agent. Your role is to help explore and understand codebases through reading files, searching for symbols, and analyzing code structure. You can only read; you cannot write or modify files.',
212212
tools: ['read_file', 'search_files', 'search_code', 'fetch_url', 'tool_search'],
213213
readonly: true,
214-
maxSteps: 30,
214+
maxSteps: 180,
215+
};
216+
217+
export const PLAN_PROFILE: AgentProfile = {
218+
name: 'plan',
219+
description:
220+
'Read-only codebase research for planning. Analyzes project structure, patterns, and dependencies to inform implementation plans. No writes.',
221+
systemPrompt: `You are a codebase research agent for planning. Your role is to analyze the codebase thoroughly before implementation begins.
222+
223+
When researching for a plan:
224+
1. Understand the project structure and conventions
225+
2. Identify relevant files and existing patterns
226+
3. Analyze dependencies and potential impacts
227+
4. Assess complexity and risks
228+
5. Check for existing implementations or similar patterns
229+
230+
Output a structured analysis covering:
231+
- **Current state assessment**: What exists today
232+
- **Key files**: Files that need modification or creation
233+
- **Dependencies and risks**: Technical debt, breaking changes, third-party concerns
234+
- **Recommended approach**: Step-by-step implementation strategy
235+
- **Implementation phases**: If the task is complex, break it into ordered phases
236+
237+
You can ONLY read files, search code, run commands, and fetch URLs. You cannot write or modify any files.`,
238+
tools: ['read_file', 'search_files', 'search_code', 'execute_command', 'fetch_url', 'tool_search'],
239+
readonly: true,
240+
maxSteps: 180,
215241
};

‎packages/codingcode/test/prompts/system-prompt.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@ describe('buildSystemPrompt', () => {
6161
expect(prompt).toContain('Read-only code exploration.');
6262
});
6363

64+
it('includes plan subagent in available subagents when provided', () => {
65+
const profiles = [
66+
{ name: 'explore', description: 'Explore.', tools: ['read_file'], disabled: false },
67+
{ name: 'plan', description: 'Codebase research for planning.', tools: ['read_file', 'search_code'], disabled: false },
68+
];
69+
const prompt = buildSystemPrompt({ ...baseOpts, agentProfiles: profiles });
70+
expect(prompt).toContain('plan');
71+
expect(prompt).toContain('Codebase research for planning');
72+
expect(prompt).toContain('dispatch_agent');
73+
});
74+
75+
it('SYSTEM_NOTES guides using plan subagent for complex tasks', () => {
76+
expect(SYSTEM_NOTES).toContain('plan');
77+
expect(SYSTEM_NOTES).toContain('dispatch_agent');
78+
expect(SYSTEM_NOTES).toContain('complex tasks');
79+
});
80+
6481
it('omits available subagents section when no profiles are provided', () => {
6582
const prompt = buildSystemPrompt(baseOpts);
6683
expect(prompt).not.toContain('Available Subagents');
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { PLAN_PROFILE, EXPLORE_PROFILE } from '../../src/subagent/registry.js';
3+
4+
describe('PLAN_PROFILE', () => {
5+
it('has name "plan"', () => {
6+
expect(PLAN_PROFILE.name).toBe('plan');
7+
});
8+
9+
it('is readonly', () => {
10+
expect(PLAN_PROFILE.readonly).toBe(true);
11+
});
12+
13+
it('has maxSteps set to 180', () => {
14+
expect(PLAN_PROFILE.maxSteps).toBe(180);
15+
});
16+
17+
it('has a systemPrompt', () => {
18+
expect(PLAN_PROFILE.systemPrompt).toBeTruthy();
19+
expect(PLAN_PROFILE.systemPrompt!.length).toBeGreaterThan(50);
20+
});
21+
22+
it('only includes read-only tools', () => {
23+
const writeTools = ['write_file', 'edit_file'];
24+
for (const wt of writeTools) {
25+
expect(PLAN_PROFILE.tools).not.toContain(wt);
26+
}
27+
});
28+
29+
it('includes read_file and search_code', () => {
30+
expect(PLAN_PROFILE.tools).toContain('read_file');
31+
expect(PLAN_PROFILE.tools).toContain('search_code');
32+
});
33+
34+
it('includes execute_command for build checks', () => {
35+
expect(PLAN_PROFILE.tools).toContain('execute_command');
36+
});
37+
38+
it('has a distinct name from explore', () => {
39+
expect(PLAN_PROFILE.name).not.toBe(EXPLORE_PROFILE.name);
40+
});
41+
42+
it('has description stating it is for planning', () => {
43+
expect(PLAN_PROFILE.description.toLowerCase()).toContain('plan');
44+
});
45+
});

‎packages/codingcode/test/tools/websearch.test.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@ describe('parseBingHtml', () => {
5656

5757
const results = parseBingHtml(html, 10);
5858
expect(results).toHaveLength(2);
59-
expect(results[0].title).toBe('Example Title 1');
60-
expect(results[0].url).toBe('https://example.com/page1');
61-
expect(results[0].snippet).toBe('Snippet text 1');
59+
expect(results[0]!.title).toBe('Example Title 1');
60+
expect(results[0]!.url).toBe('https://example.com/page1');
61+
expect(results[0]!.snippet).toBe('Snippet text 1');
6262
});
6363

6464
it('should respect maxResults limit', () => {
@@ -87,8 +87,8 @@ describe('parseBingHtml', () => {
8787
`;
8888

8989
const results = parseBingHtml(html, 10);
90-
expect(results[0].title).toBe('Bold Title');
91-
expect(results[0].snippet).toBe('Text with emphasis');
90+
expect(results[0]!.title).toBe('Bold Title');
91+
expect(results[0]!.snippet).toBe('Text with emphasis');
9292
});
9393
});
9494

0 commit comments

Comments
 (0)