Skip to content

Commit cd4aa06

Browse files
feat: k-branch parallel exploration (closes #14) (#15)
Same-round k-branch parallel exploration. Governor.run_once_parallel launches k independent worktrees per round, each running plan → execute → evaluate; the highest-fitness survivor is promoted to evolution/accepted, the rest are recorded under ledger/failed/. - config.parallel.k_branches (default 1); k=1 delegates to run_once - evaluator role emits float fitness; back-compat synthesizes fitness from hard_gates_passed when an older evaluator omits it - per-round cost/tokens are summed across all k branches for hard-stop bookkeeping - 13 new tests (67 total, 54 baseline preserved) covering parity at k=1, k>1 multi-worktree fan-out, fitness ranking, winner promotion, loser ledger demotion, worktree cleanup, cost aggregation, partial scope violation, all-fail no-promotion, and an end-to-end k=3 over 3 rounds CLI loop Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 683ec6e commit cd4aa06

12 files changed

Lines changed: 833 additions & 6 deletions

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,8 @@ flowchart LR
245245
| Config-driven: swap LLM provider, model, coding agent ||
246246
| Aider and Claude Code executor support ||
247247
| Anthropic and OpenAI planner/evaluator support ||
248-
| Goal evaluator — stops when mission is "won" | 🔧 PR #5 |
249-
| k-branch parallel exploration (FunSearch / AlphaEvolve style) | 🔧 PR #6 |
248+
| Goal evaluator — stops when mission is "won" | |
249+
| k-branch parallel exploration (FunSearch / AlphaEvolve style) | |
250250
| Process sandbox (firejail / bwrap) for production safety | 🔧 PR #7 |
251251

252252
---
@@ -293,6 +293,12 @@ coding_agent:
293293
history:
294294
max_entries: 10
295295

296+
# Population-level search: per round, spawn k independent worktrees, score
297+
# each branch's fitness, promote the best, demote the rest to ledger/failed/.
298+
# k=1 (default) is plain single-branch run_once behavior.
299+
parallel:
300+
k_branches: 1
301+
296302
roles:
297303
planner: ["python3", "roles/planner.py"]
298304
executor: ["bash", "roles/executor.sh"]

README.zh.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,8 +245,8 @@ flowchart LR
245245
| 配置驱动:随时切换 LLM 提供商、模型、coding agent ||
246246
| Aider 和 Claude Code executor 支持 ||
247247
| Anthropic 和 OpenAI 规划器 / 评估器支持 ||
248-
| 目标评估器——当 mission 完成时自动停止 | 🔧 PR #5 |
249-
| k 路并行探索(FunSearch / AlphaEvolve 模式) | 🔧 PR #6 |
248+
| 目标评估器——当 mission 完成时自动停止 | |
249+
| k 路并行探索(FunSearch / AlphaEvolve 模式) | |
250250
| 进程级沙箱(firejail / bwrap),面向生产环境 | 🔧 PR #7 |
251251

252252
---
@@ -293,6 +293,11 @@ coding_agent:
293293
history:
294294
max_entries: 10
295295

296+
# 种群级搜索:每轮起 k 个独立 worktree,按 fitness 排名,最高分推进 evolution/accepted,
297+
# 其余写入 ledger/failed/。k=1(默认)等价于单路 run_once。
298+
parallel:
299+
k_branches: 1
300+
296301
roles:
297302
planner: ["python3", "roles/planner.py"]
298303
executor: ["bash", "roles/executor.sh"]

evolution_kernel/cli.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,11 @@ def _run_with_config(args: argparse.Namespace, cfg: EvolutionConfig) -> int:
122122
print(json.dumps({"halted": True, "reason": why}, indent=2, sort_keys=True))
123123
return 3
124124

125-
result = governor.run_once(goal, run_id=args.run_id)
125+
k = cfg.parallel.k_branches
126+
if k > 1:
127+
result = governor.run_once_parallel(goal, k=k)
128+
else:
129+
result = governor.run_once(goal, run_id=args.run_id)
126130
cost_usd, tokens_used = _safe_cost(result.evaluation)
127131
new_state = hard_stops.record_outcome(
128132
state,
@@ -163,7 +167,11 @@ def _run_loop(
163167
print(json.dumps({"halted": True, "reason": why}, indent=2, sort_keys=True))
164168
return 3
165169

166-
result = governor.run_once(goal, strategy=pending_strategy)
170+
k = cfg.parallel.k_branches
171+
if k > 1:
172+
result = governor.run_once_parallel(goal, k=k, strategy=pending_strategy)
173+
else:
174+
result = governor.run_once(goal, strategy=pending_strategy)
167175
pending_strategy = None
168176
iteration += 1
169177

evolution_kernel/config.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ class StrategistConfig:
106106
every_n_rounds: int = 3
107107

108108

109+
@dataclass(frozen=True)
110+
class ParallelConfig:
111+
k_branches: int = 1
112+
113+
109114
@dataclass(frozen=True)
110115
class EvolutionConfig:
111116
mission: str
@@ -118,6 +123,7 @@ class EvolutionConfig:
118123
history: HistoryConfig = field(default_factory=HistoryConfig)
119124
goal_evaluator: GoalEvaluatorConfig = field(default_factory=GoalEvaluatorConfig)
120125
strategist: StrategistConfig = field(default_factory=StrategistConfig)
126+
parallel: ParallelConfig = field(default_factory=ParallelConfig)
121127
raw: Mapping[str, Any] = field(default_factory=dict)
122128

123129

@@ -152,6 +158,7 @@ def parse_config(raw: Mapping[str, Any]) -> EvolutionConfig:
152158
history = _parse_history(raw.get("history", {}))
153159
goal_evaluator = _parse_goal_evaluator(raw.get("goal_evaluator", {}))
154160
strategist = _parse_strategist(raw.get("strategist", {}))
161+
parallel = _parse_parallel(raw.get("parallel", {}))
155162

156163
return EvolutionConfig(
157164
mission=mission.strip(),
@@ -164,6 +171,7 @@ def parse_config(raw: Mapping[str, Any]) -> EvolutionConfig:
164171
history=history,
165172
goal_evaluator=goal_evaluator,
166173
strategist=strategist,
174+
parallel=parallel,
167175
raw=dict(raw),
168176
)
169177

@@ -313,3 +321,12 @@ def _parse_strategist(value: Any) -> StrategistConfig:
313321
if not isinstance(every_n, int) or isinstance(every_n, bool) or every_n < 1:
314322
raise ConfigError("`strategist.every_n_rounds` must be a positive integer")
315323
return StrategistConfig(enabled=bool(value.get("enabled", False)), every_n_rounds=every_n)
324+
325+
326+
def _parse_parallel(value: Any) -> ParallelConfig:
327+
if not isinstance(value, Mapping):
328+
raise ConfigError("`parallel` must be a mapping")
329+
k = value.get("k_branches", 1)
330+
if not isinstance(k, int) or isinstance(k, bool) or k < 1:
331+
raise ConfigError("`parallel.k_branches` must be a positive integer")
332+
return ParallelConfig(k_branches=k)

0 commit comments

Comments
 (0)