Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 110 additions & 7 deletions apps/mcp-server/src/agent/agent-prompt.builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ describe('agent-prompt.builder', () => {
const result = buildAgentSystemPrompt(profileWithActivation, mockContext);

expect(result).toContain('## Mandatory Checklist');
expect(result).toContain('- [ ] MUST respond in Korean');
expect(result).toContain('- [ ] Follow TDD cycle');
expect(result).toContain('- [🔴 language] MUST respond in Korean');
expect(result).toContain('- [🔴 tdd] Follow TDD cycle');
});

it('should fall back to top-level array mandatory_checklist (backward compat)', () => {
Expand All @@ -154,9 +154,9 @@ describe('agent-prompt.builder', () => {
const result = buildAgentSystemPrompt(profileWithChecklist, mockContext);

expect(result).toContain('## Mandatory Checklist');
expect(result).toContain('- [ ] Type safety: no any usage');
expect(result).toContain('- [ ] Test coverage 90%+');
expect(result).toContain('- [ ] SOLID principles applied');
expect(result).toContain('- Type safety: no any usage');
expect(result).toContain('- Test coverage 90%+');
expect(result).toContain('- SOLID principles applied');
});

it('should prefer activation.mandatory_checklist over top-level array', () => {
Expand All @@ -171,7 +171,7 @@ describe('agent-prompt.builder', () => {
};
const result = buildAgentSystemPrompt(profileWithBoth, mockContext);

expect(result).toContain('- [ ] Activation item');
expect(result).toContain('- [🔴 check] Activation item');
expect(result).not.toContain('Top-level item');
});

Expand Down Expand Up @@ -292,7 +292,8 @@ describe('agent-prompt.builder', () => {

expect(result).toContain('## Execution Order');
expect(result).toContain('1. Step 1');
expect(result).toContain('1. Step 2');
expect(result).toContain('2. Step 2');
expect(result).toContain('3. Step 3');
});

it('should handle agent without activation field gracefully', () => {
Expand All @@ -303,6 +304,108 @@ describe('agent-prompt.builder', () => {
expect(result).not.toContain('## Execution Order');
});

it('should render verification_guide array format as bullet list', () => {
const profileWithArrayGuide: AgentProfile = {
...mockAgentProfile,
activation: {
verification_guide: [
'Check test file exists before implementation',
'Verify no use client directive',
'Ensure proper error handling',
],
},
};
const result = buildAgentSystemPrompt(profileWithArrayGuide, mockContext);

expect(result).toContain('## Verification Guide');
expect(result).toContain('- Check test file exists before implementation');
expect(result).toContain('- Verify no use client directive');
expect(result).toContain('- Ensure proper error handling');
});

it('should not contain raw JSON braces in mode-specific instructions', () => {
const profileWithModes: AgentProfile = {
...mockAgentProfile,
modes: {
eval: {
focus: ['code quality', 'security'],
output_format: 'structured review',
},
},
};
const result = buildAgentSystemPrompt(profileWithModes, mockContext);

// Extract mode-specific section
const modeSection = result.split('## Mode-Specific Instructions')[1]?.split('\n##')[0] ?? '';
expect(modeSection).not.toMatch(/"[a-z_]+":/); // no JSON keys like "focus":
expect(modeSection).not.toMatch(/^\s*[{}]/m); // no lines starting with braces
});

it('should trim long mandatory checklists to 10 items with overflow indicator', () => {
const entries: Record<string, { rule: string; verification_key: string }> = {};
for (let i = 1; i <= 15; i++) {
entries[`rule_${i}`] = { rule: `Rule ${i} description`, verification_key: `key_${i}` };
}
const profileWithLargeChecklist: AgentProfile = {
...mockAgentProfile,
activation: { mandatory_checklist: entries },
};
const result = buildAgentSystemPrompt(profileWithLargeChecklist, mockContext);

expect(result).toContain('- [rule_1] Rule 1 description');
expect(result).toContain('- [rule_10] Rule 10 description');
expect(result).toContain('... and 5 more');
expect(result).not.toContain('- [rule_11]');
});

it('should trim long verification guide arrays with overflow indicator', () => {
const steps = Array.from({ length: 13 }, (_, i) => `Verify step ${i + 1}`);
const profile: AgentProfile = {
...mockAgentProfile,
activation: { verification_guide: steps },
};
const result = buildAgentSystemPrompt(profile, mockContext);

expect(result).toContain('- Verify step 1');
expect(result).toContain('- Verify step 10');
expect(result).toContain('... and 3 more');
expect(result).not.toContain('- Verify step 11');
});

it('should trim long execution order arrays with overflow indicator', () => {
const steps = Array.from({ length: 12 }, (_, i) => `Execute step ${i + 1}`);
const profile: AgentProfile = {
...mockAgentProfile,
activation: { execution_order: steps },
};
const result = buildAgentSystemPrompt(profile, mockContext);

expect(result).toContain('1. Execute step 1');
expect(result).toContain('10. Execute step 10');
expect(result).toContain('... and 2 more');
expect(result).not.toContain('11. Execute step 11');
});

it('should render mode-specific instructions as readable markdown, not JSON', () => {
const profileWithModes: AgentProfile = {
...mockAgentProfile,
modes: {
eval: {
focus: ['code quality', 'security'],
output_format: 'structured review',
depth: 3,
},
},
};
const result = buildAgentSystemPrompt(profileWithModes, mockContext);

expect(result).toContain('- **focus**:');
expect(result).toContain('- code quality');
expect(result).toContain('- security');
expect(result).toContain('- **output_format**: structured review');
expect(result).toContain('- **depth**: 3');
});

it('should handle agent without optional rich metadata gracefully', () => {
// mockAgentProfile has no mandatory_checklist, modes, skills, etc.
const result = buildAgentSystemPrompt(mockAgentProfile, mockContext);
Expand Down
82 changes: 71 additions & 11 deletions apps/mcp-server/src/agent/agent-prompt.builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,47 @@ const MODE_TASK_PREFIXES: Record<Mode, string> = {
AUTO: 'autonomous execution',
};

/**
* Maximum items to render in a list section before trimming
*/
const MAX_LIST_ITEMS = 10;

/**
* Trim a list to maxItems and append an overflow indicator
*/
function trimList(items: string[], maxItems = MAX_LIST_ITEMS): string[] {
if (items.length <= maxItems) return items;
const remaining = items.length - maxItems;
return [...items.slice(0, maxItems), `... and ${remaining} more`];
}

/**
* Render an arbitrary object/array value as readable markdown (no JSON braces)
*/
function renderValueAsText(value: unknown, sections: string[], indent = ''): void {
if (Array.isArray(value)) {
for (const item of value) {
if (item && typeof item === 'object') {
renderValueAsText(item, sections, indent);
} else {
sections.push(`${indent}- ${item}`);
}
}
} else if (value && typeof value === 'object') {
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
if (Array.isArray(val)) {
sections.push(`${indent}- **${key}**:`);
renderValueAsText(val, sections, indent + ' ');
} else if (val && typeof val === 'object') {
sections.push(`${indent}- **${key}**:`);
renderValueAsText(val, sections, indent + ' ');
} else {
sections.push(`${indent}- **${key}**: ${val}`);
}
}
}
}

/**
* Build a complete system prompt for an agent to be executed as a subagent
*/
Expand Down Expand Up @@ -86,20 +127,25 @@ export function buildAgentSystemPrompt(agentProfile: AgentProfile, context: Agen
!Array.isArray(activationChecklist)
) {
sections.push('## Mandatory Checklist');
for (const [, item] of Object.entries(activationChecklist as Record<string, unknown>)) {
const items: string[] = [];
for (const [name, item] of Object.entries(activationChecklist as Record<string, unknown>)) {
if (item && typeof item === 'object' && 'rule' in item) {
sections.push(`- [ ] ${(item as { rule: string }).rule}`);
items.push(`- [${name}] ${(item as { rule: string }).rule}`);
}
}
for (const line of trimList(items)) {
sections.push(line);
}
sections.push('');
} else if (
Array.isArray(rawProfile.mandatory_checklist) &&
rawProfile.mandatory_checklist.length
) {
// Backward compat: top-level array format
sections.push('## Mandatory Checklist');
for (const item of rawProfile.mandatory_checklist) {
sections.push(`- [ ] ${item}`);
const items = (rawProfile.mandatory_checklist as string[]).map((item: string) => `- ${item}`);
for (const line of trimList(items)) {
sections.push(line);
}
sections.push('');
}
Expand Down Expand Up @@ -144,7 +190,7 @@ export function buildAgentSystemPrompt(agentProfile: AgentProfile, context: Agen
const agentModeConfig = modesObj?.[modeKey];
if (agentModeConfig && typeof agentModeConfig === 'object') {
sections.push('## Mode-Specific Instructions');
sections.push(JSON.stringify(agentModeConfig, null, 2));
renderValueAsText(agentModeConfig, sections);
sections.push('');
} else {
sections.push(MODE_INSTRUCTIONS[context.mode]);
Expand All @@ -153,10 +199,21 @@ export function buildAgentSystemPrompt(agentProfile: AgentProfile, context: Agen

// Verification Guide — prefer activation.verification_guide
const verificationGuide = activation?.verification_guide ?? rawProfile.verification_guide;
if (verificationGuide && typeof verificationGuide === 'object') {
if (Array.isArray(verificationGuide) && verificationGuide.length) {
sections.push('## Verification Guide');
const items = (verificationGuide as string[]).map((step: string) => `- ${step}`);
for (const line of trimList(items)) {
sections.push(line);
}
sections.push('');
} else if (verificationGuide && typeof verificationGuide === 'object') {
sections.push('## Verification Guide');
const items: string[] = [];
for (const [key, desc] of Object.entries(verificationGuide as Record<string, string>)) {
sections.push(`- **${key}**: ${desc}`);
items.push(`- **${key}**: ${desc}`);
}
for (const line of trimList(items)) {
sections.push(line);
}
sections.push('');
}
Expand All @@ -168,16 +225,19 @@ export function buildAgentSystemPrompt(agentProfile: AgentProfile, context: Agen
for (const [phase, steps] of Object.entries(executionOrder as Record<string, unknown>)) {
if (Array.isArray(steps) && steps.length) {
sections.push(`### ${phase}`);
for (const step of steps) {
sections.push(`${step}`);
for (const line of trimList(steps as string[])) {
sections.push(line);
}
}
}
sections.push('');
} else if (Array.isArray(executionOrder) && executionOrder.length) {
sections.push('## Execution Order');
for (const step of executionOrder) {
sections.push(`1. ${step}`);
const items = (executionOrder as string[]).map(
(step: string, i: number) => `${i + 1}. ${step}`,
);
for (const line of trimList(items)) {
sections.push(line);
}
sections.push('');
}
Expand Down
Loading