Skip to content

feat(NanoDSLAgent): Add validation/retry mechanism and UI rendering integration #499

@phodal

Description

@phodal

NanoDSLAgent Optimization: Validation + Rendering

Summary

Enhance NanoDSLAgent with two critical features:

  1. Validation & Retry: Validate AI-generated code with NanoParser; retry if invalid
  2. UI Rendering: Render generated NanoDSL code via SketchRenderer language dispatch

Background

Currently, NanoDSLAgent generates NanoDSL code from natural language but:

  • Does not validate the generated code is syntactically correct
  • Does not render the result to the UI
  • Returns raw LLM output without quality assurance

Current Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                           mpp-core (KMP)                            │
│  ┌──────────────┐                                                   │
│  │ NanoDSLAgent │ ──LLM──> Raw Code String ──────> ToolResult       │
│  └──────────────┘            (no validation)        (no rendering)  │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                         xuiper-ui (JVM-only)                        │
│  ┌────────────┐      ┌────────────┐      ┌──────────────┐          │
│  │ NanoParser │ ───> │  NanoIR    │ ───> │ HtmlRenderer │          │
│  │ (validate) │      │ Converter  │      │ (static)     │          │
│  └────────────┘      └────────────┘      └──────────────┘          │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                         mpp-ui (Platform Renderers)                 │
│  JVM: ComposeNanoRenderer    |    VSCode: NanoRenderer.tsx          │
│  (converts NanoIR → Compose) |    (converts NanoIR → React)         │
└─────────────────────────────────────────────────────────────────────┘

Problem Statement

  1. No Validation: LLM can generate syntactically invalid NanoDSL (bad indentation, unknown components)
  2. No Retry: Failed generation is returned as-is without correction attempts
  3. No UI Rendering: Generated code is text only, never rendered visually
  4. Cross-Platform Challenge: NanoParser is JVM-only, but NanoDSLAgent runs on JS/WASM/iOS/Android

Proposed Solution

Part 1: Validation & Retry Mechanism

1.1 Create Platform-Agnostic Validation Interface

Create mpp-core/src/commonMain/.../parser/NanoDSLValidator.kt:

/**
 * Platform-agnostic NanoDSL validation result
 */
data class NanoDSLValidationResult(
    val isValid: Boolean,
    val errors: List<ValidationError> = emptyList(),
    val warnings: List<String> = emptyList()
)

data class ValidationError(
    val message: String,
    val line: Int,
    val suggestion: String? = null
)

/**
 * Basic NanoDSL validator - cross-platform
 * Performs lightweight syntax checks without full parsing
 */
expect class NanoDSLValidator() {
    fun validate(source: String): NanoDSLValidationResult
    fun parse(source: String): NanoDSLParseResult?
}

sealed class NanoDSLParseResult {
    data class Success(val irJson: String) : NanoDSLParseResult()
    data class Failure(val errors: List<ValidationError>) : NanoDSLParseResult()
}

1.2 JVM Implementation (Full Validation)

mpp-core/src/jvmMain/.../parser/NanoDSLValidator.jvm.kt:

actual class NanoDSLValidator {
    private val parser = IndentParser()
    
    actual fun validate(source: String): NanoDSLValidationResult {
        val result = parser.validate(source)
        return NanoDSLValidationResult(
            isValid = result.isValid,
            errors = result.errors.map { ValidationError(it.message, it.line, it.suggestion) },
            warnings = result.warnings.map { it.message }
        )
    }
    
    actual fun parse(source: String): NanoDSLParseResult? {
        return when (val result = parser.parse(source)) {
            is ParseResult.Success -> {
                val ir = NanoIRConverter.convert(result.ast)
                NanoDSLParseResult.Success(Json.encodeToString(ir))
            }
            is ParseResult.Failure -> {
                NanoDSLParseResult.Failure(result.errors.map { 
                    ValidationError(it.message, it.line) 
                })
            }
        }
    }
}

1.3 JS/WASM/iOS/Android Implementation (Basic Validation)

mpp-core/src/commonMain/.../parser/NanoDSLValidator.common.kt:

// Default implementation for non-JVM platforms
actual class NanoDSLValidator {
    actual fun validate(source: String): NanoDSLValidationResult {
        val errors = mutableListOf<ValidationError>()
        val lines = source.lines()
        
        // Basic checks
        if (lines.isEmpty() || lines.all { it.isBlank() }) {
            errors.add(ValidationError("Empty source", 0))
            return NanoDSLValidationResult(false, errors)
        }
        
        // Check component definition
        val firstNonBlank = lines.indexOfFirst { it.isNotBlank() }
        if (firstNonBlank >= 0) {
            val firstLine = lines[firstNonBlank].trim()
            if (!firstLine.startsWith("component ") || !firstLine.endsWith(":")) {
                errors.add(ValidationError(
                    "Missing or invalid component definition",
                    firstNonBlank + 1,
                    "Start with 'component ComponentName:'"
                ))
            }
        }
        
        // Check indentation
        lines.forEachIndexed { index, line ->
            if (line.isNotBlank()) {
                val indent = line.takeWhile { it == ' ' }.length
                if (indent % 4 != 0) {
                    errors.add(ValidationError(
                        "Inconsistent indentation (expected multiple of 4)",
                        index + 1,
                        "Use 4 spaces per level"
                    ))
                }
            }
        }
        
        return NanoDSLValidationResult(errors.isEmpty(), errors)
    }
    
    actual fun parse(source: String): NanoDSLParseResult? {
        // Non-JVM platforms cannot do full parsing
        // Return null to indicate parsing not available
        return null
    }
}

1.4 Update NanoDSLAgent with Retry Logic

class NanoDSLAgent(...) {
    private val validator = NanoDSLValidator()
    private val maxRetries = 3
    
    override suspend fun execute(
        input: NanoDSLContext,
        onProgress: (String) -> Unit
    ): ToolResult.AgentResult {
        var lastCode: String = ""
        var lastErrors: List<ValidationError> = emptyList()
        
        repeat(maxRetries) { attempt ->
            onProgress("Generation attempt ${attempt + 1}/$maxRetries")
            
            val prompt = if (attempt == 0) {
                buildPrompt(input)
            } else {
                buildRetryPrompt(input, lastCode, lastErrors)
            }
            
            val llmResponse = callLLM(prompt)
            val generatedCode = extractCode(llmResponse)
            lastCode = generatedCode
            
            // Validate
            val validationResult = validator.validate(generatedCode)
            
            if (validationResult.isValid) {
                // Try full parsing on JVM
                val parseResult = validator.parse(generatedCode)
                
                return ToolResult.AgentResult(
                    success = true,
                    content = generatedCode,
                    metadata = mapOf(
                        "attempts" to (attempt + 1).toString(),
                        "irJson" to (parseResult as? NanoDSLParseResult.Success)?.irJson.orEmpty(),
                        "hasIR" to (parseResult is NanoDSLParseResult.Success).toString()
                    )
                )
            }
            
            lastErrors = validationResult.errors
            onProgress("Validation failed: ${lastErrors.firstOrNull()?.message}")
        }
        
        // All retries exhausted
        return ToolResult.AgentResult(
            success = false,
            content = "Failed after $maxRetries attempts. Last errors:\n${lastErrors.joinToString("\n")}",
            metadata = mapOf("lastCode" to lastCode)
        )
    }
    
    private fun buildRetryPrompt(
        input: NanoDSLContext,
        previousCode: String,
        errors: List<ValidationError>
    ): String {
        return """
${buildPrompt(input)}

## Previous Attempt (INVALID - please fix):
```nanodsl
$previousCode

Validation Errors:

${errors.joinToString("\n") { "- Line ${it.line}: ${it.message}" }}

Please generate corrected NanoDSL code that fixes these errors.
""".trim()
}
}


---

### Part 2: UI Rendering via SketchRenderer (Revised Design)

> **设计修订**: 不使用 `CodingAgentRenderer.renderNanoDSL()` 方法,而是复用现有的 `SketchRenderer` 语言分发机制。

#### 2.1 现有 SketchRenderer 模式

`SketchRenderer` 已经有完善的语言分发机制:

```kotlin
// mpp-ui/src/commonMain/.../sketch/SketchRenderer.kt
when (fence.languageId.lowercase()) {
    "markdown", "md", "" -> { MarkdownSketchRenderer.RenderMarkdown(...) }
    "diff", "patch" -> { DiffSketchRenderer.RenderDiff(...) }
    "thinking" -> { ThinkingBlockRenderer(...) }
    "walkthrough" -> { WalkthroughBlockRenderer(...) }
    "mermaid", "mmd" -> { MermaidBlockRenderer(...) }
    "devin" -> { DevInBlockRenderer(...) }
    else -> { CodeBlockRenderer(...) }  // 通用代码块
}

2.2 添加 NanoDSL 语言分支

更新 mpp-ui/src/commonMain/.../sketch/SketchRenderer.kt:

when (fence.languageId.lowercase()) {
    // ... existing cases ...
    
    "nanodsl", "nano" -> {
        if (fence.text.isNotBlank()) {
            NanoDSLBlockRenderer(
                nanodslCode = fence.text,
                isComplete = blockIsComplete,
                modifier = Modifier.fillMaxWidth()
            )
            Spacer(modifier = Modifier.height(8.dp))
        }
    }
    
    else -> { CodeBlockRenderer(...) }
}

2.3 创建 NanoDSLBlockRenderer (commonMain)

mpp-ui/src/commonMain/.../sketch/NanoDSLBlockRenderer.kt:

/**
 * NanoDSL Block Renderer - Common interface
 * 
 * Renders NanoDSL code blocks with live preview on JVM platforms,
 * falls back to syntax-highlighted code display on other platforms.
 */
@Composable
expect fun NanoDSLBlockRenderer(
    nanodslCode: String,
    isComplete: Boolean = true,
    modifier: Modifier = Modifier
)

2.4 JVM 实现 (Live Preview)

mpp-ui/src/jvmMain/.../sketch/NanoDSLBlockRenderer.jvm.kt:

@Composable
actual fun NanoDSLBlockRenderer(
    nanodslCode: String,
    isComplete: Boolean,
    modifier: Modifier
) {
    var showPreview by remember { mutableStateOf(true) }
    var parseError by remember { mutableStateOf<String?>(null) }
    var nanoIR by remember { mutableStateOf<NanoIR?>(null) }
    
    // Parse NanoDSL to IR
    LaunchedEffect(nanodslCode, isComplete) {
        if (isComplete && nanodslCode.isNotBlank()) {
            try {
                val ast = NanoDSL.parse(nanodslCode)
                nanoIR = NanoDSL.toIR(ast)
                parseError = null
            } catch (e: Exception) {
                parseError = e.message
                nanoIR = null
            }
        }
    }
    
    Column(modifier = modifier) {
        // Header with toggle
        Row(
            modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Row(verticalAlignment = Alignment.CenterVertically) {
                Text("🎨", modifier = Modifier.padding(end = 8.dp))
                Text(
                    "NanoDSL UI",
                    style = MaterialTheme.typography.titleSmall,
                    color = AutoDevColors.Text.primary
                )
                if (parseError != null) {
                    Spacer(Modifier.width(8.dp))
                    Surface(
                        shape = RoundedCornerShape(4.dp),
                        color = AutoDevColors.Signal.error.copy(alpha = 0.1f)
                    ) {
                        Text(
                            "Parse Error",
                            modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
                            style = MaterialTheme.typography.labelSmall,
                            color = AutoDevColors.Signal.error
                        )
                    }
                }
            }
            
            // Toggle button (only show if we have valid IR)
            if (nanoIR != null) {
                IconButton(
                    onClick = { showPreview = !showPreview },
                    modifier = Modifier.size(28.dp)
                ) {
                    Text(
                        if (showPreview) "</>" else "👁",
                        style = MaterialTheme.typography.labelMedium
                    )
                }
            }
        }
        
        // Content
        if (showPreview && nanoIR != null) {
            // Live UI Preview
            Card(
                modifier = Modifier.fillMaxWidth().padding(8.dp),
                colors = CardDefaults.cardColors(
                    containerColor = AutoDevColors.Surface.card
                )
            ) {
                Box(modifier = Modifier.padding(16.dp)) {
                    StatefulNanoRenderer.Render(nanoIR!!)
                }
            }
        } else {
            // Source code view with syntax highlighting
            CodeBlockRenderer(
                code = nanodslCode,
                language = "nanodsl",
                displayName = "NanoDSL"
            )
        }
        
        // Show parse error details if any
        if (parseError != null && !showPreview) {
            Surface(
                modifier = Modifier.fillMaxWidth().padding(8.dp),
                color = AutoDevColors.Signal.error.copy(alpha = 0.1f),
                shape = RoundedCornerShape(4.dp)
            ) {
                Text(
                    "Parse Error: $parseError",
                    modifier = Modifier.padding(8.dp),
                    style = MaterialTheme.typography.bodySmall,
                    color = AutoDevColors.Signal.error
                )
            }
        }
    }
}

2.5 非 JVM 平台实现 (Code Only)

mpp-ui/src/jsMain/.../sketch/NanoDSLBlockRenderer.js.kt:
mpp-ui/src/wasmJsMain/.../sketch/NanoDSLBlockRenderer.wasmJs.kt:
mpp-ui/src/iosMain/.../sketch/NanoDSLBlockRenderer.ios.kt:
mpp-ui/src/androidMain/.../sketch/NanoDSLBlockRenderer.android.kt:

@Composable
actual fun NanoDSLBlockRenderer(
    nanodslCode: String,
    isComplete: Boolean,
    modifier: Modifier
) {
    Column(modifier = modifier) {
        // Header
        Row(
            modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Text("🎨", modifier = Modifier.padding(end = 8.dp))
            Text(
                "NanoDSL UI (Preview not available)",
                style = MaterialTheme.typography.titleSmall
            )
        }
        
        // Just show code (no live preview on non-JVM)
        CodeBlockRenderer(
            code = nanodslCode,
            language = "nanodsl",
            displayName = "NanoDSL"
        )
    }
}

2.6 VSCode/TypeScript 渲染

mpp-vscode/webview/src/components/sketch/NanoDSLBlock.tsx:

import { NanoRenderer } from '../nano/NanoRenderer';
import { CodeBlock } from '../CodeBlock';

interface NanoDSLBlockProps {
    code: string;
    isComplete: boolean;
}

export const NanoDSLBlock: React.FC<NanoDSLBlockProps> = ({ code, isComplete }) => {
    const [showPreview, setShowPreview] = useState(true);
    const [ir, setIR] = useState<NanoIR | null>(null);
    const [error, setError] = useState<string | null>(null);
    
    // Try to parse NanoDSL (using server-side parsing or local parser)
    useEffect(() => {
        if (isComplete && code.trim()) {
            // Option 1: Use server-side parsing via extension API
            // Option 2: Implement basic client-side parsing
            parseNanoDSL(code)
                .then(result => {
                    setIR(result);
                    setError(null);
                })
                .catch(err => {
                    setError(err.message);
                    setIR(null);
                });
        }
    }, [code, isComplete]);
    
    return (
        <div className="nano-dsl-block">
            <div className="header">
                <span className="icon">🎨</span>
                <span className="title">NanoDSL UI</span>
                {error && <span className="error-badge">Parse Error</span>}
                {ir && (
                    <button 
                        className="toggle" 
                        onClick={() => setShowPreview(!showPreview)}
                    >
                        {showPreview ? '</>' : '👁'}
                    </button>
                )}
            </div>
            
            {showPreview && ir ? (
                <div className="preview">
                    <NanoRenderer ir={ir} />
                </div>
            ) : (
                <CodeBlock code={code} language="nanodsl" />
            )}
            
            {error && !showPreview && (
                <div className="error-details">Parse Error: {error}</div>
            )}
        </div>
    );
};

2.7 更新 TypeScript SketchRenderer

mpp-ui/src/jsMain/typescript/ui/SketchRenderer.tsx (如果存在) 或相关文件:

// In the language switch case
case 'nanodsl':
case 'nano':
    return <NanoDSLBlock code={fence.text} isComplete={fence.isComplete} />;

设计对比

❌ 原方案: CodingAgentRenderer.renderNanoDSL()

问题:
1. 需要修改 CodingAgentRenderer 接口 (所有实现都要改)
2. 需要添加 TimelineItem.NanoDSLItem
3. 与现有 SketchRenderer 机制重复
4. Agent 需要显式调用 renderer.renderNanoDSL()

✅ 新方案: SketchRenderer 语言分发

优势:
1. 复用现有 SketchRenderer 机制
2. 只需添加语言分支 + 一个 expect/actual 组件
3. 自动处理 ```nanodsl 代码块
4. 与 mermaid、diff 等渲染方式一致
5. LLM 输出直接包含 ```nanodsl 代码块即可

Implementation Checklist

Phase 1: Validation & Retry

  • Create NanoDSLValidator expect/actual classes in mpp-core
  • Implement JVM version with full parsing via xuiper-ui
  • Implement basic validation for JS/WASM/iOS/Android
  • Update NanoDSLAgent with retry logic
  • Add error feedback in retry prompt
  • Add tests for validation and retry

Phase 2: Rendering via SketchRenderer

  • Add "nanodsl", "nano" case to SketchRenderer.kt
  • Create NanoDSLBlockRenderer expect declaration (commonMain)
  • Implement JVM version with live preview (jvmMain)
  • Implement JS/WASM/iOS/Android versions (code only)
  • Create VSCode NanoDSLBlock.tsx component
  • Update TypeScript sketch rendering if needed
  • Add integration tests

Documentation

  • Add NanoDSL rendering documentation

Files to Modify

mpp-core (Kotlin Multiplatform)

  • src/commonMain/.../subagent/NanoDSLAgent.kt - Add validation & retry
  • src/commonMain/.../parser/NanoDSLValidator.kt - New file (expect)
  • src/jvmMain/.../parser/NanoDSLValidator.jvm.kt - New file (actual)
  • src/jsMain/.../parser/NanoDSLValidator.js.kt - New file (actual)
  • (similar for wasmJs, ios, android)

mpp-ui (Compose)

  • src/commonMain/.../sketch/SketchRenderer.kt - Add nanodsl case
  • src/commonMain/.../sketch/NanoDSLBlockRenderer.kt - New file (expect)
  • src/jvmMain/.../sketch/NanoDSLBlockRenderer.jvm.kt - New file (actual, with live preview)
  • src/jsMain/.../sketch/NanoDSLBlockRenderer.js.kt - New file (actual, code only)
  • src/wasmJsMain/.../sketch/NanoDSLBlockRenderer.wasmJs.kt - New file (actual)
  • src/iosMain/.../sketch/NanoDSLBlockRenderer.ios.kt - New file (actual)
  • src/androidMain/.../sketch/NanoDSLBlockRenderer.android.kt - New file (actual)

mpp-vscode (VSCode Extension)

  • webview/src/components/sketch/NanoDSLBlock.tsx - New file
  • Update sketch rendering to handle nanodsl language

xuiper-ui (JVM-only parser)

  • No changes needed - already has NanoParser/NanoIR

Acceptance Criteria

  1. Validation Works:

    • Valid NanoDSL code passes validation on all platforms
    • Invalid code returns meaningful error messages
    • JVM platforms perform full AST parsing
  2. Retry Works:

    • Agent retries up to 3 times on validation failure
    • Retry prompt includes previous errors
    • Success after retry is tracked in metadata
  3. Rendering Works:

    • ```nanodsl 代码块在 JVM 平台显示 live preview
    • 非 JVM 平台显示语法高亮的代码
    • VSCode 显示交互式预览 (如果可解析)
    • 解析错误时显示错误信息 + 原始代码
  4. Cross-Platform:

    • All platforms can generate and validate NanoDSL
    • JVM platforms get full parsing + preview
    • Non-JVM platforms get basic validation + code display

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions