From e16054508c40e01514e4dd11178b7e28e3d22e9e Mon Sep 17 00:00:00 2001 From: JeremyDev87 Date: Sat, 4 Apr 2026 10:30:18 +0900 Subject: [PATCH] improve(mcp): render agent prompts as compact execution guides (#1260) Render mandatory_checklist as `- [name] rule` bullets, verification_guide as bullet list (array + object), execution_order with incremental numbering. Replace JSON.stringify for mode-specific instructions with readable markdown. Add trimList helper to cap long sections at 10 items + overflow indicator. --- .../src/agent/agent-prompt.builder.spec.ts | 117 ++++++++++++++++-- .../src/agent/agent-prompt.builder.ts | 82 ++++++++++-- 2 files changed, 181 insertions(+), 18 deletions(-) diff --git a/apps/mcp-server/src/agent/agent-prompt.builder.spec.ts b/apps/mcp-server/src/agent/agent-prompt.builder.spec.ts index 36de237a..2484dbfb 100644 --- a/apps/mcp-server/src/agent/agent-prompt.builder.spec.ts +++ b/apps/mcp-server/src/agent/agent-prompt.builder.spec.ts @@ -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)', () => { @@ -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', () => { @@ -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'); }); @@ -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', () => { @@ -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 = {}; + 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); diff --git a/apps/mcp-server/src/agent/agent-prompt.builder.ts b/apps/mcp-server/src/agent/agent-prompt.builder.ts index d222a8f8..e331dde5 100644 --- a/apps/mcp-server/src/agent/agent-prompt.builder.ts +++ b/apps/mcp-server/src/agent/agent-prompt.builder.ts @@ -41,6 +41,47 @@ const MODE_TASK_PREFIXES: Record = { 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)) { + 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 */ @@ -86,11 +127,15 @@ export function buildAgentSystemPrompt(agentProfile: AgentProfile, context: Agen !Array.isArray(activationChecklist) ) { sections.push('## Mandatory Checklist'); - for (const [, item] of Object.entries(activationChecklist as Record)) { + const items: string[] = []; + for (const [name, item] of Object.entries(activationChecklist as Record)) { 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) && @@ -98,8 +143,9 @@ export function buildAgentSystemPrompt(agentProfile: AgentProfile, context: Agen ) { // 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(''); } @@ -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]); @@ -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)) { - sections.push(`- **${key}**: ${desc}`); + items.push(`- **${key}**: ${desc}`); + } + for (const line of trimList(items)) { + sections.push(line); } sections.push(''); } @@ -168,16 +225,19 @@ export function buildAgentSystemPrompt(agentProfile: AgentProfile, context: Agen for (const [phase, steps] of Object.entries(executionOrder as Record)) { 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(''); }