Skip to content
2 changes: 1 addition & 1 deletion mpp-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ kotlin {
// Kotlin Logging for multiplatform logging
implementation("io.github.oshai:kotlin-logging:7.0.13")

runtimeOnly("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1-0.6.x-compat")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1-0.6.x-compat")
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The dependency change from runtimeOnly to implementation for kotlinx-datetime is correct and necessary since the library is now directly used in the commonMain source set (e.g., in AgentPlan.kt, PlanStep.kt). However, consider whether this should use a version catalog or be consistent with other datetime usages in the project.

Suggested change
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.7.1-0.6.x-compat")
implementation(libs.kotlinx.datetime)

Copilot uses AI. Check for mistakes.
// Koog AI Framework - JVM only for now
implementation("ai.koog:koog-agents:0.5.2")
implementation("ai.koog:agents-mcp:0.5.2")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,48 @@ All tools use the DevIns format with JSON parameters:
```
</devin>

# Planning and Task Management

For complex multi-step tasks, use the `/plan` tool to create and track progress:

## When to Use Planning
- Tasks requiring multiple files to be created or modified
- Tasks with dependencies between steps
- Tasks that benefit from structured tracking

## Plan Format
```markdown
1. Task Title
- [ ] Step 1 description
- [ ] Step 2 description

2. Another Task
- [ ] Step description
```

## Plan Actions
- `CREATE`: Create a new plan with markdown content
- `COMPLETE_STEP`: Mark a step as done (taskIndex=1, stepIndex=1 for first step of first task)
- `VIEW`: View current plan status

Example:
<devin>
/plan
```json
{"action": "CREATE", "planMarkdown": "1. Setup\n - [ ] Create entity class\n - [ ] Create repository\n\n2. Implementation\n - [ ] Create service\n - [ ] Create controller"}
```
</devin>

Comment on lines +58 to +70
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The system prompt guidance for the plan tool uses 1-based indexing (line 60: "taskIndex=1, stepIndex=1 for first step of first task"), but this should be more clearly documented in the tool's own description to avoid confusion. The PlanManagementTool description (lines 169-170) mentions "1-based" but could be more prominent.

Consider adding explicit examples with different index values to make it crystal clear:

Examples:
- First task, first step: taskIndex=1, stepIndex=1
- First task, second step: taskIndex=1, stepIndex=2  
- Second task, first step: taskIndex=2, stepIndex=1

Copilot uses AI. Check for mistakes.
# Task Completion Strategy

**IMPORTANT: Focus on completing the task efficiently.**

1. **Understand the Task**: Read the user's request carefully
2. **Gather Minimum Required Information**: Only collect information directly needed for the task
3. **Execute the Task**: Make the necessary changes or provide the answer
4. **Verify if Needed**: For code changes, compile/test to verify
5. **Provide Summary**: Always end with a clear summary of what was done
2. **Plan if Complex**: For multi-step tasks, create a plan first using `/plan`
3. **Gather Minimum Required Information**: Only collect information directly needed for the task
4. **Execute the Task**: Make the necessary changes, marking steps complete as you go
5. **Verify if Needed**: For code changes, compile/test to verify
6. **Provide Summary**: Always end with a clear summary of what was done

**Avoid over-exploration**: Don't spend iterations exploring unrelated code. Stay focused on the task.

Expand Down Expand Up @@ -161,15 +194,48 @@ ${'$'}{toolList}
```
</devin>

# 计划和任务管理

对于复杂的多步骤任务,使用 `/plan` 工具来创建和跟踪进度:

## 何时使用计划
- 需要创建或修改多个文件的任务
- 步骤之间有依赖关系的任务
- 需要结构化跟踪的任务

## 计划格式
```markdown
1. 任务标题
- [ ] 步骤1描述
- [ ] 步骤2描述

2. 另一个任务
- [ ] 步骤描述
```

## 计划操作
- `CREATE`: 使用 markdown 内容创建新计划
- `COMPLETE_STEP`: 标记步骤完成 (taskIndex=1, stepIndex=1 表示第一个任务的第一个步骤)
- `VIEW`: 查看当前计划状态

示例:
<devin>
/plan
```json
{"action": "CREATE", "planMarkdown": "1. 设置\n - [ ] 创建实体类\n - [ ] 创建仓库\n\n2. 实现\n - [ ] 创建服务\n - [ ] 创建控制器"}
```
</devin>

# 任务完成策略

**重要:专注于高效完成任务。**

1. **理解任务**:仔细阅读用户的请求
2. **收集最少必要信息**:只收集任务直接需要的信息
3. **执行任务**:进行必要的更改或提供答案
4. **必要时验证**:对于代码更改,编译/测试以验证
5. **提供总结**:始终以清晰的总结结束
2. **复杂任务先计划**:对于多步骤任务,先使用 `/plan` 创建计划
3. **收集最少必要信息**:只收集任务直接需要的信息
4. **执行任务**:进行必要的更改,完成后标记步骤
5. **必要时验证**:对于代码更改,编译/测试以验证
6. **提供总结**:始终以清晰的总结结束

**避免过度探索**:不要花费迭代次数探索无关代码。保持专注于任务。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ import cc.unitmesh.agent.orchestrator.ToolExecutionContext as OrchestratorContex
data class AsyncShellConfig(
/** Initial wait timeout in milliseconds before notifying AI that process is still running */
val initialWaitTimeoutMs: Long = 60_000L, // 1 minute
/** Maximum total wait time in milliseconds */
val maxWaitTimeoutMs: Long = 300_000L, // 5 minutes
/** Maximum total wait time in milliseconds (2 minutes, similar to Cursor/Claude Code) */
val maxWaitTimeoutMs: Long = 120_000L, // 2 minutes
/** Interval for checking process status after initial timeout */
val checkIntervalMs: Long = 30_000L // 30 seconds
)
Expand Down Expand Up @@ -212,7 +212,8 @@ class CodingAgentExecutor(

val executionContext = OrchestratorContext(
workingDirectory = projectPath,
environment = emptyMap()
environment = emptyMap(),
timeout = asyncShellConfig.maxWaitTimeoutMs // Use max timeout for shell commands
)

var executionResult = toolOrchestrator.executeToolCall(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ class ToolOrchestrator(
else -> {
// Handle special tools that need parameter conversion
when (toolName.lowercase()) {
"task-boundary" -> executeTaskBoundaryTool(tool, params, basicContext)
"plan" -> executePlanManagementTool(tool, params, basicContext)
"docql" -> executeDocQLTool(tool, params, basicContext)
else -> {
// For truly generic tools, use generic execution
Expand Down Expand Up @@ -675,26 +675,37 @@ class ToolOrchestrator(
return invocation.execute(context)
}

private suspend fun executeTaskBoundaryTool(
private suspend fun executePlanManagementTool(
tool: Tool,
params: Map<String, Any>,
context: cc.unitmesh.agent.tool.ToolExecutionContext
): ToolResult {
val taskBoundaryTool = tool as cc.unitmesh.agent.tool.impl.TaskBoundaryTool

val taskName = params["taskName"] as? String
?: return ToolResult.Error("taskName parameter is required")
val status = params["status"] as? String
?: return ToolResult.Error("status parameter is required")
val summary = params["summary"] as? String ?: ""

val taskBoundaryParams = cc.unitmesh.agent.tool.impl.TaskBoundaryParams(
taskName = taskName,
status = status,
summary = summary
val planTool = tool as cc.unitmesh.agent.tool.impl.PlanManagementTool

val action = params["action"] as? String
?: return ToolResult.Error("action parameter is required")
val planMarkdown = params["planMarkdown"] as? String ?: ""

// Handle taskIndex and stepIndex - can be Number or String
val taskIndex = when (val v = params["taskIndex"]) {
is Number -> v.toInt()
is String -> v.toIntOrNull() ?: 0
else -> 0
}
val stepIndex = when (val v = params["stepIndex"]) {
is Number -> v.toInt()
is String -> v.toIntOrNull() ?: 0
else -> 0
Comment on lines +691 to +698
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The executePlanManagementTool function uses inconsistent parameter conversion that could lead to errors. When taskIndex or stepIndex are missing from the params map, they default to 0, which will cause validation errors later (line 87 checks for <= 0).

Consider explicitly handling missing parameters:

val taskIndex = when (val v = params["taskIndex"]) {
    null -> return ToolResult.Error("taskIndex parameter is required for COMPLETE_STEP and FAIL_STEP")
    is Number -> v.toInt()
    is String -> v.toIntOrNull() ?: return ToolResult.Error("taskIndex must be a valid integer")
    else -> return ToolResult.Error("taskIndex has invalid type")
}

This would provide clearer error messages to the agent.

Suggested change
is Number -> v.toInt()
is String -> v.toIntOrNull() ?: 0
else -> 0
}
val stepIndex = when (val v = params["stepIndex"]) {
is Number -> v.toInt()
is String -> v.toIntOrNull() ?: 0
else -> 0
null -> return ToolResult.Error("taskIndex parameter is required for COMPLETE_STEP and FAIL_STEP")
is Number -> v.toInt()
is String -> v.toIntOrNull() ?: return ToolResult.Error("taskIndex must be a valid integer")
else -> return ToolResult.Error("taskIndex has invalid type")
}
val stepIndex = when (val v = params["stepIndex"]) {
null -> return ToolResult.Error("stepIndex parameter is required for COMPLETE_STEP and FAIL_STEP")
is Number -> v.toInt()
is String -> v.toIntOrNull() ?: return ToolResult.Error("stepIndex must be a valid integer")
else -> return ToolResult.Error("stepIndex has invalid type")

Copilot uses AI. Check for mistakes.
}

val planParams = cc.unitmesh.agent.tool.impl.PlanManagementParams(
action = action,
planMarkdown = planMarkdown,
taskIndex = taskIndex,
stepIndex = stepIndex
)
val invocation = taskBoundaryTool.createInvocation(taskBoundaryParams)

val invocation = planTool.createInvocation(planParams)
return invocation.execute(context)
}
Comment on lines +678 to 710
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd -t f "PlanManagementTool.kt" --exec cat -n {}

Repository: phodal/auto-dev-sketch

Length of output: 10270


Address invalid default indices for step operations.

The taskIndex and stepIndex default to 0, but PlanManagementTool enforces 1-based indexing and validates that both must be positive (line 87 in PlanManagementTool.kt). This causes COMPLETE_STEP and FAIL_STEP actions to fail with error "taskIndex and stepIndex must be positive" whenever indices are not explicitly provided. Consider:

  • Making these parameters required for operations that need them (COMPLETE_STEP, FAIL_STEP)
  • Or use null as default and validate at invocation creation time
  • Or document that defaults only apply to CREATE/UPDATE/VIEW operations
🤖 Prompt for AI Agents
In
mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/orchestrator/ToolOrchestrator.kt
around lines 678 to 710, the current code defaults taskIndex and stepIndex to 0
which breaks PlanManagementTool because it expects 1-based positive indices;
change handling so indices are nullable by default and validate/require them for
actions that need them: parse params["taskIndex"] and params["stepIndex"] into
Int? (null if missing or unparsable), then when building PlanManagementParams
set taskIndex/stepIndex to the nullable values (or call a constructor that
accepts nullable), and before creating the invocation check the action: for
COMPLETE_STEP and FAIL_STEP (and any other actions that require indices) return
ToolResult.Error if the corresponding index is null or <=0; for
CREATE/UPDATE/VIEW keep behavior that treats null as absent. Ensure validation
error messages match PlanManagementTool expectations.


Expand All @@ -704,16 +715,16 @@ class ToolOrchestrator(
context: cc.unitmesh.agent.tool.ToolExecutionContext
): ToolResult {
val docqlTool = tool as cc.unitmesh.agent.tool.impl.DocQLTool

val query = params["query"] as? String
?: return ToolResult.Error("query parameter is required")
val documentPath = params["documentPath"] as? String // Optional

val docqlParams = cc.unitmesh.agent.tool.impl.DocQLParams(
query = query,
documentPath = documentPath
)

val invocation = docqlTool.createInvocation(docqlParams)
return invocation.execute(context)
}
Expand All @@ -738,7 +749,7 @@ class ToolOrchestrator(

/**
* Execute generic tool using ExecutableTool interface
* This handles new tools like task-boundary, ask-agent, etc. without needing specific implementations
* This handles new tools like ask-agent, etc. without needing specific implementations
*/
private suspend fun executeGenericTool(
tool: Tool,
Expand Down
140 changes: 140 additions & 0 deletions mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/plan/AgentPlan.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package cc.unitmesh.agent.plan

import kotlinx.datetime.Clock
import kotlinx.serialization.Serializable

/**
* Represents a complete agent plan containing multiple tasks.
*
* An AgentPlan is the top-level container for organizing work
* into tasks and steps, with tracking for creation and update times.
*/
@Serializable
data class AgentPlan(
/**
* Unique identifier for this plan
*/
val id: String,

/**
* Tasks in this plan
*/
val tasks: MutableList<PlanTask> = mutableListOf(),

/**
* Timestamp when this plan was created (epoch milliseconds)
*/
val createdAt: Long = Clock.System.now().toEpochMilliseconds(),

/**
* Timestamp when this plan was last updated (epoch milliseconds)
*/
var updatedAt: Long = createdAt
) {
/**
* Overall status of the plan (derived from tasks)
*/
val status: TaskStatus
get() = when {
tasks.isEmpty() -> TaskStatus.TODO
tasks.all { it.status == TaskStatus.COMPLETED } -> TaskStatus.COMPLETED
tasks.any { it.status == TaskStatus.FAILED } -> TaskStatus.FAILED
tasks.any { it.status == TaskStatus.IN_PROGRESS } -> TaskStatus.IN_PROGRESS
tasks.any { it.status == TaskStatus.BLOCKED } -> TaskStatus.BLOCKED
else -> TaskStatus.TODO
}

/**
* Overall progress percentage (0-100)
*/
val progressPercent: Int
get() {
val totalSteps = tasks.sumOf { it.totalStepCount }
if (totalSteps == 0) return 0
val completedSteps = tasks.sumOf { it.completedStepCount }
return (completedSteps * 100) / totalSteps
}

/**
* Total number of tasks
*/
val taskCount: Int
get() = tasks.size

/**
* Number of completed tasks
*/
val completedTaskCount: Int
get() = tasks.count { it.isCompleted }

/**
* Add a task to this plan
*/
fun addTask(task: PlanTask) {
tasks.add(task)
touch()
}

/**
* Get a task by ID
*/
fun getTask(taskId: String): PlanTask? {
return tasks.find { it.id == taskId }
}

/**
* Update a task's status
*/
fun updateTaskStatus(taskId: String, status: TaskStatus) {
getTask(taskId)?.updateStatus(status)
touch()
}

/**
* Complete a step within a task
*/
fun completeStep(taskId: String, stepId: String) {
getTask(taskId)?.completeStep(stepId)
touch()
}

/**
* Update the updatedAt timestamp
*/
private fun touch() {
updatedAt = Clock.System.now().toEpochMilliseconds()
}

/**
* Convert to markdown format
*/
fun toMarkdown(): String {
val sb = StringBuilder()
tasks.forEachIndexed { index, task ->
sb.append(task.toMarkdown(index + 1))
}
return sb.toString()
}

companion object {
private var idCounter = 0L

/**
* Create a new plan with generated ID
*/
fun create(tasks: List<PlanTask> = emptyList()): AgentPlan {
return AgentPlan(
id = generateId(),
tasks = tasks.toMutableList()
)
}

/**
* Generate a unique plan ID
*/
fun generateId(): String {
return "plan_${++idCounter}_${Clock.System.now().toEpochMilliseconds()}"
}
}
}

Loading
Loading