Skip to content

Commit 2af4664

Browse files
committed
feat(cli): add Coding Agent CLI for autonomous coding tasks
Introduce a new Gradle task and CLI entrypoint to run CodingAgent tasks from the command line. Includes a console renderer and configuration via ~/.autodev/config.yaml. Also clarifies CodingAgentTemplate completion instructions.
1 parent c86af0e commit 2af4664

File tree

3 files changed

+284
-6
lines changed

3 files changed

+284
-6
lines changed

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/CodingAgentTemplate.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,11 @@ ${'$'}{toolList}
242242
243243
# 任务完成
244244
245-
当任务完成时:
246-
- 对于**分析任务**:提供清晰、结构化的发现总结
247-
- 对于**代码更改**:确认更改已完成并验证
248-
- 如果不需要更多工具调用,使用 `/reply` 提供最终总结
245+
当任务完成时,在响应中直接提供清晰的总结(无需工具调用):
246+
- 对于**分析任务**:以结构化格式列出你的发现
247+
- 对于**代码更改**:确认更改了什么以及已验证
248+
249+
如果你已完成任务,直接回复你的总结,不要包含任何 <devin> 块。
249250
250251
#if (${'$'}{agentRules})
251252
# 项目特定规则

mpp-ui/build.gradle.kts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,29 @@ tasks.register<JavaExec>("runDocumentCli") {
497497
standardInput = System.`in`
498498
}
499499

500+
// Task to run Coding CLI
501+
tasks.register<JavaExec>("runCodingCli") {
502+
group = "application"
503+
description = "Run Coding Agent CLI for autonomous coding tasks"
504+
505+
val jvmCompilation = kotlin.jvm().compilations.getByName("main")
506+
classpath(jvmCompilation.output, configurations["jvmRuntimeClasspath"])
507+
mainClass.set("cc.unitmesh.server.cli.CodingCli")
508+
509+
// Pass properties - use codingProjectPath to avoid conflict with Gradle's projectPath
510+
if (project.hasProperty("codingProjectPath")) {
511+
systemProperty("projectPath", project.property("codingProjectPath") as String)
512+
}
513+
if (project.hasProperty("codingTask")) {
514+
systemProperty("task", project.property("codingTask") as String)
515+
}
516+
if (project.hasProperty("codingMaxIterations")) {
517+
systemProperty("maxIterations", project.property("codingMaxIterations") as String)
518+
}
519+
520+
standardInput = System.`in`
521+
}
522+
500523
// Task to run Review CLI
501524
tasks.register<JavaExec>("runReviewCli") {
502525
group = "application"
@@ -525,7 +548,7 @@ tasks.register<JavaExec>("runReviewCli") {
525548
if (project.hasProperty("reviewLanguage")) {
526549
systemProperty("reviewLanguage", project.property("reviewLanguage") as String)
527550
}
528-
551+
529552
standardInput = System.`in`
530553
}
531554

@@ -548,7 +571,7 @@ tasks.register<JavaExec>("runDomainDictCli") {
548571
if (project.hasProperty("domainFocusArea")) {
549572
systemProperty("domainFocusArea", project.property("domainFocusArea") as String)
550573
}
551-
574+
552575
standardInput = System.`in`
553576
}
554577

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
package cc.unitmesh.server.cli
2+
3+
import cc.unitmesh.agent.AgentTask
4+
import cc.unitmesh.agent.CodingAgent
5+
import cc.unitmesh.agent.config.McpToolConfigService
6+
import cc.unitmesh.agent.config.ToolConfigFile
7+
import cc.unitmesh.agent.render.CodingAgentRenderer
8+
import cc.unitmesh.agent.tool.filesystem.DefaultToolFileSystem
9+
import cc.unitmesh.llm.KoogLLMService
10+
import cc.unitmesh.llm.LLMProviderType
11+
import cc.unitmesh.llm.ModelConfig
12+
import cc.unitmesh.llm.compression.TokenInfo
13+
import com.charleskorn.kaml.Yaml
14+
import kotlinx.coroutines.runBlocking
15+
import java.io.File
16+
17+
/**
18+
* JVM CLI for testing CodingAgent with autonomous coding tasks
19+
*
20+
* Usage:
21+
* ```bash
22+
* ./gradlew :mpp-ui:runCodingCli -PcodingProjectPath=/path/to/project -PcodingTask="Add a hello world function"
23+
* ```
24+
*/
25+
object CodingCli {
26+
27+
@JvmStatic
28+
fun main(args: Array<String>) {
29+
println("=".repeat(80))
30+
println("AutoDev Coding Agent CLI (JVM)")
31+
println("=".repeat(80))
32+
33+
// Parse arguments
34+
val projectPath = System.getProperty("projectPath") ?: args.getOrNull(0) ?: run {
35+
System.err.println("Usage: -PcodingProjectPath=<path> -PcodingTask=<task> [-PmaxIterations=100]")
36+
return
37+
}
38+
39+
val task = System.getProperty("task") ?: args.getOrNull(1) ?: run {
40+
System.err.println("Usage: -PcodingProjectPath=<path> -PcodingTask=<task> [-PmaxIterations=100]")
41+
return
42+
}
43+
44+
val maxIterations = System.getProperty("maxIterations")?.toIntOrNull() ?: 100
45+
46+
println("📂 Project Path: $projectPath")
47+
println("📝 Task: $task")
48+
println("🔄 Max Iterations: $maxIterations")
49+
println()
50+
51+
runBlocking {
52+
try {
53+
val projectDir = File(projectPath).absoluteFile
54+
if (!projectDir.exists()) {
55+
System.err.println("❌ Project path does not exist: $projectPath")
56+
return@runBlocking
57+
}
58+
59+
val startTime = System.currentTimeMillis()
60+
61+
// Load configuration from ~/.autodev/config.yaml
62+
val configFile = File(System.getProperty("user.home"), ".autodev/config.yaml")
63+
if (!configFile.exists()) {
64+
System.err.println("❌ Configuration file not found: ${configFile.absolutePath}")
65+
System.err.println(" Please create ~/.autodev/config.yaml with your LLM configuration")
66+
return@runBlocking
67+
}
68+
69+
val yamlContent = configFile.readText()
70+
val yaml = Yaml(configuration = com.charleskorn.kaml.YamlConfiguration(strictMode = false))
71+
val config = yaml.decodeFromString(AutoDevConfig.serializer(), yamlContent)
72+
73+
val activeName = config.active
74+
val activeConfig = config.configs.find { it.name == activeName }
75+
76+
if (activeConfig == null) {
77+
System.err.println("❌ Active configuration '$activeName' not found in config.yaml")
78+
System.err.println(" Available configs: ${config.configs.map { it.name }.joinToString(", ")}")
79+
return@runBlocking
80+
}
81+
82+
println("📝 Using config: ${activeConfig.name} (${activeConfig.provider}/${activeConfig.model})")
83+
84+
// Convert provider string to LLMProviderType
85+
val providerType = when (activeConfig.provider.lowercase()) {
86+
"openai" -> LLMProviderType.OPENAI
87+
"anthropic" -> LLMProviderType.ANTHROPIC
88+
"google" -> LLMProviderType.GOOGLE
89+
"deepseek" -> LLMProviderType.DEEPSEEK
90+
"ollama" -> LLMProviderType.OLLAMA
91+
"openrouter" -> LLMProviderType.OPENROUTER
92+
"glm" -> LLMProviderType.GLM
93+
"qwen" -> LLMProviderType.QWEN
94+
"kimi" -> LLMProviderType.KIMI
95+
else -> LLMProviderType.CUSTOM_OPENAI_BASE
96+
}
97+
98+
val llmService = KoogLLMService(
99+
ModelConfig(
100+
provider = providerType,
101+
modelName = activeConfig.model,
102+
apiKey = activeConfig.apiKey,
103+
temperature = activeConfig.temperature ?: 0.7,
104+
maxTokens = activeConfig.maxTokens ?: 4096,
105+
baseUrl = activeConfig.baseUrl ?: ""
106+
)
107+
)
108+
109+
val renderer = CodingCliRenderer()
110+
val mcpConfigService = McpToolConfigService(ToolConfigFile())
111+
112+
println("🧠 Creating CodingAgent...")
113+
val agent = CodingAgent(
114+
projectPath = projectDir.absolutePath,
115+
llmService = llmService,
116+
maxIterations = maxIterations,
117+
renderer = renderer,
118+
fileSystem = DefaultToolFileSystem(projectDir.absolutePath),
119+
mcpToolConfigService = mcpConfigService,
120+
enableLLMStreaming = true
121+
)
122+
123+
println("✅ Agent created")
124+
println()
125+
println("🚀 Executing task...")
126+
println()
127+
128+
val result = agent.execute(
129+
AgentTask(requirement = task, projectPath = projectDir.absolutePath),
130+
onProgress = { progress -> println(" $progress") }
131+
)
132+
133+
val totalTime = System.currentTimeMillis() - startTime
134+
135+
println()
136+
println("=".repeat(80))
137+
println("📊 Result:")
138+
println("=".repeat(80))
139+
println(result.content)
140+
println()
141+
142+
if (result.success) {
143+
println("✅ Task completed successfully")
144+
} else {
145+
println("❌ Task failed")
146+
}
147+
println("⏱️ Total time: ${totalTime}ms")
148+
println("📈 Steps: ${result.metadata["steps"] ?: "N/A"}")
149+
println("✏️ Edits: ${result.metadata["edits"] ?: "N/A"}")
150+
151+
} catch (e: Exception) {
152+
System.err.println("❌ Error: ${e.message}")
153+
e.printStackTrace()
154+
}
155+
}
156+
}
157+
}
158+
159+
/**
160+
* Console renderer for CodingCli output
161+
*/
162+
class CodingCliRenderer : CodingAgentRenderer {
163+
override fun renderIterationHeader(current: Int, max: Int) {
164+
println("\n━━━ Iteration $current/$max ━━━")
165+
}
166+
167+
override fun renderLLMResponseStart() {
168+
println("💭 ")
169+
}
170+
171+
override fun renderLLMResponseChunk(chunk: String) {
172+
print(chunk)
173+
System.out.flush()
174+
}
175+
176+
override fun renderLLMResponseEnd() {
177+
println("\n")
178+
}
179+
180+
override fun renderToolCall(toolName: String, paramsStr: String) {
181+
println("$toolName")
182+
if (paramsStr.isNotEmpty()) {
183+
val formatted = formatCliParameters(paramsStr)
184+
formatted.lines().forEach { line ->
185+
println("$line")
186+
}
187+
}
188+
}
189+
190+
private fun formatCliParameters(params: String): String {
191+
val trimmed = params.trim()
192+
193+
// Handle JSON format
194+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
195+
val lines = mutableListOf<String>()
196+
val jsonPattern = Regex(""""(\w+)"\s*:\s*("([^"]*)"|(\d+)|true|false|null)""")
197+
jsonPattern.findAll(trimmed).forEach { match ->
198+
val key = match.groups[1]?.value ?: ""
199+
val value = match.groups[3]?.value
200+
?: match.groups[4]?.value
201+
?: match.groups[2]?.value?.removeSurrounding("\"")
202+
?: ""
203+
lines.add("$key = $value")
204+
}
205+
return if (lines.isNotEmpty()) lines.joinToString(", ") else params
206+
}
207+
208+
return params
209+
}
210+
211+
override fun renderToolResult(
212+
toolName: String,
213+
success: Boolean,
214+
output: String?,
215+
fullOutput: String?,
216+
metadata: Map<String, String>
217+
) {
218+
val statusSymbol = if (success) "" else ""
219+
val preview = (output ?: fullOutput ?: "").lines().take(3).joinToString(" ").take(100)
220+
println(" $statusSymbol ${if (preview.length < (output ?: fullOutput ?: "").length) "$preview..." else preview}")
221+
}
222+
223+
override fun renderTaskComplete() {
224+
println("\n✓ Task marked as complete")
225+
}
226+
227+
override fun renderFinalResult(success: Boolean, message: String, iterations: Int) {
228+
val symbol = if (success) "" else ""
229+
println("\n$symbol Final result after $iterations iterations:")
230+
println(message)
231+
}
232+
233+
override fun renderError(message: String) {
234+
System.err.println("❌ Error: $message")
235+
}
236+
237+
override fun renderRepeatWarning(toolName: String, count: Int) {
238+
println("⚠️ Warning: Tool '$toolName' called $count times")
239+
}
240+
241+
override fun renderRecoveryAdvice(recoveryAdvice: String) {
242+
println("💡 Recovery advice: $recoveryAdvice")
243+
}
244+
245+
override fun updateTokenInfo(tokenInfo: TokenInfo) {
246+
// Display token info in CLI
247+
println("📊 Tokens: ${tokenInfo.inputTokens} in / ${tokenInfo.outputTokens} out")
248+
}
249+
250+
override fun renderUserConfirmationRequest(toolName: String, params: Map<String, Any>) {
251+
println("❓ Confirmation required for: $toolName")
252+
println(" Params: $params")
253+
}
254+
}

0 commit comments

Comments
 (0)