Skip to content

Commit 0e8a480

Browse files
committed
feat(agent): improve plan step rendering and orchestration
Refactor rendering logic and update plan step handling across agent components to enhance orchestration and output formatting.
1 parent 2e17b3a commit 0e8a480

File tree

11 files changed

+370
-39
lines changed

11 files changed

+370
-39
lines changed

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ For complex multi-step tasks, use the `/plan` tool to create and track progress:
6060
- `COMPLETE_STEP`: Mark a step as done (taskIndex=1, stepIndex=1 for first step of first task)
6161
- `VIEW`: View current plan status
6262
63+
## IMPORTANT: Plan Update Rules
64+
- **Mark ONE step at a time**: After completing actual work, call COMPLETE_STEP for that single step only
65+
- **Do NOT batch multiple COMPLETE_STEP calls**: Each response should contain at most ONE plan update
66+
- **Update after work is done**: Only mark a step complete AFTER you have actually performed the work
67+
6368
Example:
6469
<devin>
6570
/plan
@@ -128,10 +133,15 @@ When a tool fails:
128133
129134
# IMPORTANT: One Tool Per Response
130135
131-
**Execute ONLY ONE tool per response.**
136+
**Execute ONLY ONE tool per response. This is critical for proper execution.**
132137
133138
- ✅ CORRECT: One <devin> block with ONE tool call
134-
- ❌ WRONG: Multiple <devin> blocks
139+
- ❌ WRONG: Multiple <devin> blocks or multiple tool calls
140+
141+
**Special note for /plan tool:**
142+
- Do NOT call multiple COMPLETE_STEP in one response
143+
- Complete one step, wait for confirmation, then proceed to next step
144+
- Each plan update requires a separate response cycle
135145
136146
# Response Format
137147

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

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ package cc.unitmesh.agent.executor
33
import cc.unitmesh.agent.*
44
import cc.unitmesh.agent.conversation.ConversationManager
55
import cc.unitmesh.agent.core.SubAgentManager
6+
import cc.unitmesh.agent.logging.getLogger
67
import cc.unitmesh.agent.tool.schema.ToolResultFormatter
78
import cc.unitmesh.agent.orchestrator.ToolExecutionResult
89
import cc.unitmesh.agent.orchestrator.ToolOrchestrator
910
import cc.unitmesh.agent.render.CodingAgentRenderer
1011
import cc.unitmesh.agent.state.ToolCall
1112
import cc.unitmesh.agent.state.ToolExecutionState
13+
import cc.unitmesh.agent.plan.PlanSummaryData
1214
import cc.unitmesh.agent.tool.ToolResult
1315
import cc.unitmesh.agent.tool.ToolType
1416
import cc.unitmesh.agent.tool.toToolType
@@ -38,7 +40,13 @@ class CodingAgentExecutor(
3840
maxIterations: Int = 100,
3941
private val subAgentManager: SubAgentManager? = null,
4042
enableLLMStreaming: Boolean = true,
41-
private val asyncShellConfig: AsyncShellConfig = AsyncShellConfig()
43+
private val asyncShellConfig: AsyncShellConfig = AsyncShellConfig(),
44+
/**
45+
* When true, only execute the first tool call per LLM response.
46+
* This enforces the "one tool per response" rule even when LLM returns multiple tool calls.
47+
* Default is true to prevent LLM from executing multiple tools in one iteration.
48+
*/
49+
private val singleToolPerIteration: Boolean = true
4250
) : BaseAgentExecutor(
4351
projectPath = projectPath,
4452
llmService = llmService,
@@ -47,6 +55,7 @@ class CodingAgentExecutor(
4755
maxIterations = maxIterations,
4856
enableLLMStreaming = enableLLMStreaming
4957
) {
58+
private val logger = getLogger("CodingAgentExecutor")
5059
private val steps = mutableListOf<AgentStep>()
5160
private val edits = mutableListOf<AgentEdit>()
5261

@@ -92,12 +101,22 @@ class CodingAgentExecutor(
92101
break
93102
}
94103

95-
val toolCalls = toolCallParser.parseToolCalls(llmResponse.toString())
96-
if (toolCalls.isEmpty()) {
104+
val allToolCalls = toolCallParser.parseToolCalls(llmResponse.toString())
105+
if (allToolCalls.isEmpty()) {
97106
renderer.renderTaskComplete()
98107
break
99108
}
100109

110+
// When singleToolPerIteration is enabled, only execute the first tool call
111+
// This enforces the "one tool per response" rule even when LLM returns multiple tool calls
112+
val toolCalls = if (singleToolPerIteration && allToolCalls.size > 1) {
113+
logger.warn { "LLM returned ${allToolCalls.size} tool calls, but singleToolPerIteration is enabled. Only executing the first one: ${allToolCalls.first().toolName}" }
114+
renderer.renderError("Warning: LLM returned ${allToolCalls.size} tool calls, only executing the first one")
115+
listOf(allToolCalls.first())
116+
} else {
117+
allToolCalls
118+
}
119+
101120
val toolResults = executeToolCalls(toolCalls)
102121
val toolResultsText = ToolResultFormatter.formatMultipleToolResults(toolResults)
103122
conversationManager!!.addToolResults(toolResultsText)
@@ -274,6 +293,11 @@ class CodingAgentExecutor(
274293
executionResult.metadata
275294
)
276295

296+
// Render plan summary bar after plan tool execution
297+
if (toolName == "plan" && executionResult.isSuccess) {
298+
renderPlanSummaryIfAvailable()
299+
}
300+
277301
val currentToolType = toolName.toToolType()
278302
if ((currentToolType == ToolType.WriteFile) && executionResult.isSuccess) {
279303
recordFileEdit(params)
@@ -512,4 +536,14 @@ class CodingAgentExecutor(
512536
fun getConversationHistory(): List<cc.unitmesh.devins.llm.Message> {
513537
return conversationManager?.getHistory() ?: emptyList()
514538
}
539+
540+
/**
541+
* Render plan summary bar if a plan is available
542+
*/
543+
private fun renderPlanSummaryIfAvailable() {
544+
val planStateService = toolOrchestrator.getPlanStateService() ?: return
545+
val currentPlan = planStateService.currentPlan.value ?: return
546+
val summary = PlanSummaryData.from(currentPlan)
547+
renderer.renderPlanSummary(summary)
548+
}
515549
}

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

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ class ToolOrchestrator(
3939
) {
4040
private val logger = getLogger("cc.unitmesh.agent.orchestrator.ToolOrchestrator")
4141

42+
/**
43+
* Get the PlanStateService from the registered PlanManagementTool.
44+
* Returns null if no plan tool is registered.
45+
*/
46+
fun getPlanStateService(): cc.unitmesh.agent.plan.PlanStateService? {
47+
val planTool = registry.getTool("plan") as? cc.unitmesh.agent.tool.impl.PlanManagementTool
48+
return planTool?.getPlanStateService()
49+
}
50+
4251
// Coroutine scope for background tasks (async shell monitoring)
4352
private val backgroundScope = CoroutineScope(Dispatchers.Default)
4453

@@ -698,17 +707,65 @@ class ToolOrchestrator(
698707
else -> 0
699708
}
700709

710+
// Handle batch steps parameter
711+
val steps = parseStepsParameter(params["steps"])
712+
701713
val planParams = cc.unitmesh.agent.tool.impl.PlanManagementParams(
702714
action = action,
703715
planMarkdown = planMarkdown,
704716
taskIndex = taskIndex,
705-
stepIndex = stepIndex
717+
stepIndex = stepIndex,
718+
steps = steps
706719
)
707720

708721
val invocation = planTool.createInvocation(planParams)
709722
return invocation.execute(context)
710723
}
711724

725+
/**
726+
* Parse the steps parameter which can be:
727+
* - A List of Maps with taskIndex and stepIndex
728+
* - A JSON string representing an array
729+
* - null or empty
730+
*/
731+
private fun parseStepsParameter(stepsParam: Any?): List<cc.unitmesh.agent.tool.impl.StepRef> {
732+
if (stepsParam == null) return emptyList()
733+
734+
return when (stepsParam) {
735+
is List<*> -> {
736+
stepsParam.mapNotNull { item ->
737+
when (item) {
738+
is Map<*, *> -> {
739+
val taskIdx = when (val t = item["taskIndex"]) {
740+
is Number -> t.toInt()
741+
is String -> t.toIntOrNull() ?: return@mapNotNull null
742+
else -> return@mapNotNull null
743+
}
744+
val stepIdx = when (val s = item["stepIndex"]) {
745+
is Number -> s.toInt()
746+
is String -> s.toIntOrNull() ?: return@mapNotNull null
747+
else -> return@mapNotNull null
748+
}
749+
cc.unitmesh.agent.tool.impl.StepRef(taskIdx, stepIdx)
750+
}
751+
else -> null
752+
}
753+
}
754+
}
755+
is String -> {
756+
// Try to parse as JSON array
757+
try {
758+
val parsed = kotlinx.serialization.json.Json.decodeFromString<List<cc.unitmesh.agent.tool.impl.StepRef>>(stepsParam)
759+
parsed
760+
} catch (e: Exception) {
761+
logger.debug { "Failed to parse steps parameter as JSON: ${e.message}" }
762+
emptyList()
763+
}
764+
}
765+
else -> emptyList()
766+
}
767+
}
768+
712769
private suspend fun executeDocQLTool(
713770
tool: Tool,
714771
params: Map<String, Any>,

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ package cc.unitmesh.agent.plan
2020
object MarkdownPlanParser {
2121

2222
// Pattern for task headers: "1. Task Title" or "1. [x] Task Title"
23+
// Note: The closing bracket ] must be escaped as \\] for JavaScript compatibility
2324
private val TASK_HEADER_PATTERN = Regex("^(\\d+)\\.\\s*(?:\\[([xX!*✓]?)\\]\\s*)?(.+?)(?:\\s*\\[([xX!*✓]?)\\])?$")
2425

2526
// Pattern for step items: "- [x] Step description"
26-
private val STEP_PATTERN = Regex("^\\s*[-*]\\s*\\[\\s*([xX!*✓]?)\\s*]\\s*(.*)")
27+
// Note: The closing bracket ] must be escaped as \\] for JavaScript compatibility
28+
private val STEP_PATTERN = Regex("^\\s*[-*]\\s*\\[\\s*([xX!*✓]?)\\s*\\]\\s*(.*)")
2729

2830
// Pattern for unordered list items without checkbox: "- Step description"
2931
private val UNORDERED_ITEM_PATTERN = Regex("^\\s*[-*]\\s+(.+)")

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ data class PlanStep(
7676
}
7777

7878
companion object {
79-
private val STEP_PATTERN = Regex("^\\s*-\\s*\\[\\s*([xX!*✓]?)\\s*]\\s*(.*)")
79+
// Note: The closing bracket ] must be escaped as \\] for JavaScript compatibility
80+
private val STEP_PATTERN = Regex("^\\s*-\\s*\\[\\s*([xX!*✓]?)\\s*\\]\\s*(.*)")
8081

8182
/**
8283
* Parse a step from markdown text

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package cc.unitmesh.agent.render
22

3+
import cc.unitmesh.agent.plan.PlanSummaryData
34
import cc.unitmesh.agent.tool.ToolResult
45
import cc.unitmesh.llm.compression.TokenInfo
56

@@ -27,6 +28,21 @@ interface CodingAgentRenderer {
2728

2829
fun updateTokenInfo(tokenInfo: TokenInfo) {}
2930

31+
/**
32+
* Render a compact plan summary bar.
33+
* Called when plan is created or updated to show progress in a compact format.
34+
*
35+
* Example display:
36+
* ```
37+
* 📋 Plan: Create Tag System (3/5 steps, 60%) ████████░░░░░░░░
38+
* ```
39+
*
40+
* @param summary The plan summary data containing progress information
41+
*/
42+
fun renderPlanSummary(summary: PlanSummaryData) {
43+
// Default: no-op for renderers that don't support plan summary bar
44+
}
45+
3046
fun renderUserConfirmationRequest(toolName: String, params: Map<String, Any>)
3147

3248
/**

0 commit comments

Comments
 (0)