feat: Add cost-aware parallel execution with execution_group support

- Schema: Add execution_group and task-level complexity fields
- Executor: Hybrid dependency analysis (explicit + file conflicts)
- Executor: Cost-based batching (MAX_BATCH_COST=8, Low=1/Medium=2/High=4)
- Executor: execution_group priority for explicit parallel grouping
- Planner: Add guidance for execution_group and complexity fields

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
catlog22
2025-11-28 12:45:10 +08:00
parent 98b72f086d
commit cde17bd668
3 changed files with 83 additions and 43 deletions

View File

@@ -149,14 +149,14 @@ Input Parsing:
Execution: Execution:
├─ Step 1: Initialize result tracking (previousExecutionResults = []) ├─ Step 1: Initialize result tracking (previousExecutionResults = [])
├─ Step 2: Task grouping & batch creation ├─ Step 2: Task grouping & batch creation (cost-aware)
│ ├─ Extract explicit depends_on (no file/keyword inference) │ ├─ Build hybrid dependencies (explicit depends_on + file conflicts via modification_points)
│ ├─ Group: independent tasks → single parallel batch (maximize utilization) │ ├─ Phase 1: Group by execution_group (explicit parallel groups)
│ ├─ Group: dependent tasks → sequential phases (respect dependencies) │ ├─ Phase 2: Cost-aware batching (MAX_BATCH_COST=8, Low=1/Medium=2/High=4)
│ └─ Create TodoWrite list for batches │ └─ Create TodoWrite list for batches
├─ Step 3: Launch execution ├─ Step 3: Launch execution
│ ├─ Phase 1: All independent tasks (⚡ single batch, concurrent) │ ├─ Parallel batches: ⚡ concurrent via multiple tool calls
│ └─ Phase 2+: Dependent tasks by dependency order │ └─ Sequential batches: → one by one
├─ Step 4: Track progress (TodoWrite updates per batch) ├─ Step 4: Track progress (TodoWrite updates per batch)
└─ Step 5: Code review (if codeReviewTool ≠ "Skip") └─ Step 5: Code review (if codeReviewTool ≠ "Skip")
@@ -181,67 +181,96 @@ previousExecutionResults = []
**Dependency Analysis & Grouping Algorithm**: **Dependency Analysis & Grouping Algorithm**:
```javascript ```javascript
// Use explicit depends_on from plan.json (no inference from file/keywords) const MAX_BATCH_COST = 8 // Workload limit per batch (tunable)
function extractDependencies(tasks) {
// Task cost based on complexity (Low=1, Medium=2, High=4)
function calculateTaskCost(task) {
const costs = { Low: 1, Medium: 2, High: 4 }
return costs[task.complexity] || 1
}
// Build hybrid dependencies: explicit depends_on + implicit file conflicts
function buildDependencies(tasks) {
const taskIdToIndex = {} const taskIdToIndex = {}
tasks.forEach((t, i) => { taskIdToIndex[t.id] = i }) tasks.forEach((t, i) => { taskIdToIndex[t.id] = i })
const fileOwner = {} // Track which task last modified each file
return tasks.map((task, i) => { return tasks.map((task, i) => {
// Only use explicit depends_on from plan.json const deps = new Set()
const deps = (task.depends_on || [])
.map(depId => taskIdToIndex[depId]) // 1. Explicit depends_on
.filter(idx => idx !== undefined && idx < i) ;(task.depends_on || []).forEach(depId => {
return { ...task, taskIndex: i, dependencies: deps } const idx = taskIdToIndex[depId]
if (idx !== undefined && idx < i) deps.add(idx)
})
// 2. Implicit file conflicts via modification_points
;(task.modification_points || []).forEach(mp => {
if (fileOwner[mp.file] !== undefined && fileOwner[mp.file] !== i) {
deps.add(fileOwner[mp.file])
}
fileOwner[mp.file] = i
})
return { ...task, taskIndex: i, dependencies: [...deps], cost: calculateTaskCost(task) }
}) })
} }
// Group into batches: maximize parallel execution // Group into cost-aware batches with execution_group priority
function createExecutionCalls(tasks, executionMethod) { function createExecutionCalls(tasks, executionMethod) {
const tasksWithDeps = extractDependencies(tasks) const tasksWithDeps = buildDependencies(tasks)
const processed = new Set() const processed = new Set()
const calls = [] const calls = []
let parallelIdx = 1, sequentialIdx = 1
// Phase 1: All independent tasks → single parallel batch (maximize utilization) // Phase 1: Group by execution_group (explicit parallel groups)
const independentTasks = tasksWithDeps.filter(t => t.dependencies.length === 0) const groups = {}
if (independentTasks.length > 0) { tasksWithDeps.forEach(t => {
independentTasks.forEach(t => processed.add(t.taskIndex)) if (t.execution_group) {
groups[t.execution_group] = groups[t.execution_group] || []
groups[t.execution_group].push(t)
}
})
Object.entries(groups).forEach(([groupId, groupTasks]) => {
groupTasks.forEach(t => processed.add(t.taskIndex))
calls.push({ calls.push({
method: executionMethod, method: executionMethod,
executionType: "parallel", executionType: "parallel",
groupId: "P1", groupId: `P${parallelIdx++}`,
taskSummary: independentTasks.map(t => t.title).join(' | '), taskSummary: groupTasks.map(t => t.title).join(' | '),
tasks: independentTasks tasks: groupTasks
}) })
} })
// Phase 2: Dependent tasks → sequential batches (respect dependencies) // Phase 2: Process remaining by dependency order with cost-aware batching
let sequentialIndex = 1
let remaining = tasksWithDeps.filter(t => !processed.has(t.taskIndex)) let remaining = tasksWithDeps.filter(t => !processed.has(t.taskIndex))
while (remaining.length > 0) { while (remaining.length > 0) {
// Find tasks whose dependencies are all satisfied const ready = remaining.filter(t => t.dependencies.every(d => processed.has(d)))
const ready = remaining.filter(t => if (ready.length === 0) { ready.push(...remaining) } // Break circular deps
t.dependencies.every(d => processed.has(d))
)
if (ready.length === 0) { // Cost-aware batch creation
console.warn('Circular dependency detected, forcing remaining tasks') let batch = [], batchCost = 0
ready.push(...remaining) const stillReady = [...ready]
while (stillReady.length > 0) {
const idx = stillReady.findIndex(t => batchCost + t.cost <= MAX_BATCH_COST)
if (idx === -1) break
const task = stillReady.splice(idx, 1)[0]
batch.push(task)
batchCost += task.cost
} }
if (batch.length === 0) batch = [ready[0]] // At least one task
// Group ready tasks (can run in parallel within this phase) batch.forEach(t => processed.add(t.taskIndex))
ready.forEach(t => processed.add(t.taskIndex))
calls.push({ calls.push({
method: executionMethod, method: executionMethod,
executionType: ready.length > 1 ? "parallel" : "sequential", executionType: batch.length > 1 ? "parallel" : "sequential",
groupId: ready.length > 1 ? `P${calls.length + 1}` : `S${sequentialIndex++}`, groupId: batch.length > 1 ? `P${parallelIdx++}` : `S${sequentialIdx++}`,
taskSummary: ready.map(t => t.title).join(ready.length > 1 ? ' | ' : ' → '), taskSummary: batch.map(t => t.title).join(batch.length > 1 ? ' | ' : ' → '),
tasks: ready tasks: batch
}) })
remaining = remaining.filter(t => !processed.has(t.taskIndex)) remaining = remaining.filter(t => !processed.has(t.taskIndex))
} }
return calls return calls
} }
@@ -539,8 +568,8 @@ codex --full-auto exec "[Verify plan acceptance criteria at ${plan.json}]" --ski
## Best Practices ## Best Practices
**Input Modes**: In-memory (lite-plan), prompt (standalone), file (JSON/text) **Input Modes**: In-memory (lite-plan), prompt (standalone), file (JSON/text)
**Task Grouping**: Based on explicit depends_on only; independent tasks run in single parallel batch **Task Grouping**: Hybrid dependencies (explicit depends_on + file conflicts) + execution_group priority + cost-aware batching (MAX_BATCH_COST=8)
**Execution**: All independent tasks launch concurrently via single Claude message with multiple tool calls **Execution**: Parallel batches via single Claude message with multiple tool calls; cost balances workload (Low=1, Medium=2, High=4)
## Error Handling ## Error Handling

View File

@@ -341,7 +341,7 @@ const schema = Bash(`cat ~/.claude/workflows/cli-templates/schemas/plan-json-sch
const plan = { const plan = {
summary: "...", summary: "...",
approach: "...", approach: "...",
tasks: [...], // Follow Task Grouping Rules below tasks: [...], // Each task: { id, title, scope, ..., depends_on, execution_group, complexity }
estimated_time: "...", estimated_time: "...",
recommended_execution: "Agent", recommended_execution: "Agent",
complexity: "Low", complexity: "Low",
@@ -412,6 +412,8 @@ Generate plan.json with:
3. **Substantial tasks**: Each task should represent 15-60 minutes of work 3. **Substantial tasks**: Each task should represent 15-60 minutes of work
4. **True dependencies only**: Only use depends_on when Task B cannot start without Task A's output 4. **True dependencies only**: Only use depends_on when Task B cannot start without Task A's output
5. **Prefer parallel**: Most tasks should be independent (no depends_on) 5. **Prefer parallel**: Most tasks should be independent (no depends_on)
6. **Explicit parallel groups**: Assign same `execution_group` ID (e.g., "group-1") to tasks that can run concurrently; null for others
7. **Task complexity**: Set `complexity` (Low/Medium/High) for workload balancing - executor uses cost units (1/2/4)
## Execution ## Execution
1. Read ALL exploration files for comprehensive context 1. Read ALL exploration files for comprehensive context

View File

@@ -118,6 +118,15 @@
"pattern": "^T[0-9]+$" "pattern": "^T[0-9]+$"
}, },
"description": "Task IDs this task depends on (e.g., ['T1', 'T2'])" "description": "Task IDs this task depends on (e.g., ['T1', 'T2'])"
},
"execution_group": {
"type": ["string", "null"],
"description": "Parallel execution group ID. Tasks with same group ID run concurrently. null = use depends_on logic"
},
"complexity": {
"type": "string",
"enum": ["Low", "Medium", "High"],
"description": "Task complexity for workload balancing (Low=1, Medium=2, High=4 cost units)"
} }
} }
}, },