-
Notifications
You must be signed in to change notification settings - Fork 480
Description
NanoDSLAgent Optimization: Validation + Rendering
Summary
Enhance NanoDSLAgent with two critical features:
- Validation & Retry: Validate AI-generated code with NanoParser; retry if invalid
- UI Rendering: Render generated NanoDSL code via
SketchRendererlanguage 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
- No Validation: LLM can generate syntactically invalid NanoDSL (bad indentation, unknown components)
- No Retry: Failed generation is returned as-is without correction attempts
- No UI Rendering: Generated code is text only, never rendered visually
- Cross-Platform Challenge:
NanoParseris JVM-only, butNanoDSLAgentruns 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
$previousCodeValidation 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
NanoDSLValidatorexpect/actual classes in mpp-core - Implement JVM version with full parsing via xuiper-ui
- Implement basic validation for JS/WASM/iOS/Android
- Update
NanoDSLAgentwith retry logic - Add error feedback in retry prompt
- Add tests for validation and retry
Phase 2: Rendering via SketchRenderer
- Add
"nanodsl", "nano"case toSketchRenderer.kt - Create
NanoDSLBlockRendererexpect declaration (commonMain) - Implement JVM version with live preview (jvmMain)
- Implement JS/WASM/iOS/Android versions (code only)
- Create VSCode
NanoDSLBlock.tsxcomponent - 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 & retrysrc/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 casesrc/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
-
Validation Works:
- Valid NanoDSL code passes validation on all platforms
- Invalid code returns meaningful error messages
- JVM platforms perform full AST parsing
-
Retry Works:
- Agent retries up to 3 times on validation failure
- Retry prompt includes previous errors
- Success after retry is tracked in metadata
-
Rendering Works:
```nanodsl代码块在 JVM 平台显示 live preview- 非 JVM 平台显示语法高亮的代码
- VSCode 显示交互式预览 (如果可解析)
- 解析错误时显示错误信息 + 原始代码
-
Cross-Platform:
- All platforms can generate and validate NanoDSL
- JVM platforms get full parsing + preview
- Non-JVM platforms get basic validation + code display