|
| 1 | +# branching — Copy-on-Write Workspace Branching for AI Agents |
| 2 | + |
| 3 | +Explore multiple strategies without consequences. Fork the workspace into |
| 4 | +parallel copy-on-write branches, run speculative attempts in each, commit |
| 5 | +the winner, and abort the rest — instantly. |
| 6 | + |
| 7 | +## Workspace |
| 8 | + |
| 9 | +A workspace is a directory backed by a copy-on-write filesystem (branchfs |
| 10 | +or daxfs). All branching operations happen within a workspace. |
| 11 | + |
| 12 | +**Prerequisite:** The workspace must be mounted before use. Two backends |
| 13 | +are supported, auto-detected at runtime: |
| 14 | + |
| 15 | +| Backend | Mount | |
| 16 | +|---------|-------| |
| 17 | +| [BranchFS](https://github.com/multikernel/branchfs) (FUSE) | `branchfs /mnt/workspace` | |
| 18 | +| [DaxFS](https://github.com/multikernel/daxfs) (kernel) | `mount -t daxfs <device> /mnt/workspace` | |
| 19 | + |
| 20 | +The CLI auto-detects the workspace from your current directory if you are |
| 21 | +inside a branchfs/daxfs mount. Otherwise pass `-w /mnt/workspace`. |
| 22 | + |
| 23 | +## CLI |
| 24 | + |
| 25 | +### branching run — single branch execution |
| 26 | + |
| 27 | +Run a command in an isolated branch. Commits on exit 0, aborts on non-zero. |
| 28 | + |
| 29 | +```bash |
| 30 | +branching run -- ./build.sh |
| 31 | +branching run --ask -- make test # prompt before commit/abort |
| 32 | +branching run --on-error none -- python train.py # keep branch on failure |
| 33 | +branching run --memory-limit 512M -- ./agent.sh |
| 34 | +``` |
| 35 | + |
| 36 | +### branching speculate — race N approaches |
| 37 | + |
| 38 | +Race multiple commands in parallel. First success wins, rest are aborted. |
| 39 | + |
| 40 | +Use when multiple approaches could work and you want the fastest success. |
| 41 | + |
| 42 | +```bash |
| 43 | +branching speculate -c "./fix_a.sh" -c "./fix_b.sh" -c "./fix_c.sh" |
| 44 | +branching speculate --timeout 60 -c "python v1.py" -c "python v2.py" |
| 45 | +``` |
| 46 | + |
| 47 | +### branching best-of-n — parallel with scoring |
| 48 | + |
| 49 | +Run the same command N times in parallel. Commit the highest-scoring success. |
| 50 | +Child writes score to fd 3 (`echo 0.95 >&3`). Gets `BRANCHING_ATTEMPT` env var. |
| 51 | + |
| 52 | +Use when quality matters: pick the best result across multiple attempts. |
| 53 | + |
| 54 | +```bash |
| 55 | +branching best-of-n -n 5 -- ./solve.py |
| 56 | +branching best-of-n -n 3 -- bash -c 'python run.py && echo "$SCORE" >&3' |
| 57 | +``` |
| 58 | + |
| 59 | +### branching reflexion — retry with feedback |
| 60 | + |
| 61 | +Sequential retry with critique. Child gets `BRANCHING_ATTEMPT` and |
| 62 | +`BRANCHING_FEEDBACK` (empty on first try, critique output on retries). |
| 63 | + |
| 64 | +Use when failure output helps improve the next attempt. |
| 65 | + |
| 66 | +```bash |
| 67 | +branching reflexion --retries 5 -- ./fix.sh |
| 68 | +branching reflexion --retries 3 --critique "./review.sh" -- ./solve.py |
| 69 | +``` |
| 70 | + |
| 71 | +### branching status — workspace info |
| 72 | + |
| 73 | +```bash |
| 74 | +branching status |
| 75 | +branching status --json |
| 76 | +``` |
| 77 | + |
| 78 | +### Common CLI flags |
| 79 | + |
| 80 | +| Flag | Description | |
| 81 | +|------|-------------| |
| 82 | +| `-w PATH` | Workspace path (auto-detected from cwd if omitted) | |
| 83 | +| `-b NAME` | Branch name (auto-generated if omitted) | |
| 84 | +| `--json` | Machine-readable JSON output | |
| 85 | +| `--timeout SEC` | Timeout for parallel commands | |
| 86 | +| `--memory-limit SIZE` | Per-branch memory cap (e.g. `512M`, `1G`) | |
| 87 | +| `--cpu-limit FRAC` | Per-branch CPU limit (e.g. `0.5` = 50%) | |
| 88 | +| `--group-memory-limit SIZE` | Total memory budget for all branches | |
| 89 | +| `--group-cpu-limit FRAC` | Total CPU budget for all branches | |
| 90 | + |
| 91 | +## Python API |
| 92 | + |
| 93 | +Install: `pip install branchcontext` |
| 94 | + |
| 95 | +### Workspace and Branch |
| 96 | + |
| 97 | +```python |
| 98 | +from branching import Workspace |
| 99 | + |
| 100 | +ws = Workspace("/mnt/workspace") |
| 101 | + |
| 102 | +# Auto-commit on success, auto-abort on exception |
| 103 | +with ws.branch("attempt") as b: |
| 104 | + # b.path is an isolated copy-on-write view of the workspace |
| 105 | + subprocess.run(["agent", "--workdir", str(b.path)], check=True) |
| 106 | + |
| 107 | +# Manual control |
| 108 | +with ws.branch("attempt", on_success=None, on_error=None) as b: |
| 109 | + result = run_agent(workdir=b.path) |
| 110 | + if result.confident: |
| 111 | + b.commit() |
| 112 | + else: |
| 113 | + b.abort() |
| 114 | + |
| 115 | +# Nested branches |
| 116 | +with ws.branch("strategy_a") as a: |
| 117 | + apply_strategy(a.path) |
| 118 | + with a.branch("variant_1") as v1: |
| 119 | + tweak(v1.path) |
| 120 | + # v1 auto-commits into a on success |
| 121 | + # a auto-commits into workspace on success |
| 122 | +``` |
| 123 | + |
| 124 | +### Speculate — race N candidates, first wins |
| 125 | + |
| 126 | +```python |
| 127 | +from branching import Workspace, Speculate |
| 128 | + |
| 129 | +def try_fix_a(path: Path) -> bool: |
| 130 | + apply_patch(path / "a.patch") |
| 131 | + return run_tests(path) |
| 132 | + |
| 133 | +def try_fix_b(path: Path) -> bool: |
| 134 | + apply_patch(path / "b.patch") |
| 135 | + return run_tests(path) |
| 136 | + |
| 137 | +outcome = Speculate([try_fix_a, try_fix_b], first_wins=True, timeout=60)(ws) |
| 138 | +if outcome.committed: |
| 139 | + print(f"Fix {outcome.winner.branch_index} succeeded!") |
| 140 | +``` |
| 141 | + |
| 142 | +### BestOfN — parallel attempts, highest score wins |
| 143 | + |
| 144 | +```python |
| 145 | +from branching import BestOfN |
| 146 | + |
| 147 | +def scored_task(path: Path, attempt: int) -> tuple[bool, float]: |
| 148 | + result = run_agent(workdir=path, seed=attempt) |
| 149 | + return result.passed, result.quality_score |
| 150 | + |
| 151 | +outcome = BestOfN(scored_task, n=5)(ws) |
| 152 | +``` |
| 153 | + |
| 154 | +### Reflexion — retry with critique feedback |
| 155 | + |
| 156 | +```python |
| 157 | +from branching import Reflexion |
| 158 | + |
| 159 | +def task(path: Path, attempt: int, feedback: str | None) -> bool: |
| 160 | + if feedback: |
| 161 | + (path / "critique.txt").write_text(feedback) |
| 162 | + return run_and_test(path) |
| 163 | + |
| 164 | +def critique(path: Path) -> str: |
| 165 | + return analyze_failure(path / "test_output.log") |
| 166 | + |
| 167 | +outcome = Reflexion(task, max_retries=3, critique=critique)(ws) |
| 168 | +``` |
| 169 | + |
| 170 | +### TreeOfThoughts — hierarchical strategy exploration |
| 171 | + |
| 172 | +```python |
| 173 | +from branching import TreeOfThoughts |
| 174 | + |
| 175 | +def strategy_a(path: Path) -> tuple[bool, float]: |
| 176 | + apply_approach_a(path) |
| 177 | + return run_tests(path), evaluate_quality(path) |
| 178 | + |
| 179 | +outcome = TreeOfThoughts( |
| 180 | + [strategy_a, strategy_b], |
| 181 | + expand=lambda path, depth: generate_refinements(path), |
| 182 | + max_depth=2, |
| 183 | +)(ws) |
| 184 | +``` |
| 185 | + |
| 186 | +### BeamSearch — multi-level with top-K survival |
| 187 | + |
| 188 | +```python |
| 189 | +from branching import BeamSearch |
| 190 | + |
| 191 | +outcome = BeamSearch( |
| 192 | + [strat_a, strat_b, strat_c, strat_d], |
| 193 | + expand=lambda path, depth: generate_refinements(path), |
| 194 | + beam_width=2, |
| 195 | + max_depth=3, |
| 196 | +)(ws) |
| 197 | +``` |
| 198 | + |
| 199 | +### Tournament — pairwise elimination |
| 200 | + |
| 201 | +```python |
| 202 | +from branching import Tournament |
| 203 | + |
| 204 | +def generate_patch(path: Path, index: int) -> bool: |
| 205 | + return run_agent(workdir=path, seed=index) |
| 206 | + |
| 207 | +def judge(path_a: Path, path_b: Path) -> int: |
| 208 | + # 0 = a wins, 1 = b wins |
| 209 | + return llm_compare(path_a / "diff.patch", path_b / "diff.patch") |
| 210 | + |
| 211 | +outcome = Tournament(generate_patch, n=8, judge=judge)(ws) |
| 212 | +``` |
| 213 | + |
| 214 | +### Process isolation and resource limits |
| 215 | + |
| 216 | +```python |
| 217 | +from branching import BranchContext, ResourceLimits |
| 218 | + |
| 219 | +# Run untrusted code in a sandboxed child process |
| 220 | +with ws.branch("sandboxed", on_success=None, on_error=None) as b: |
| 221 | + with BranchContext(run_untrusted, workspace=b.path) as ctx: |
| 222 | + ctx.wait(timeout=30) |
| 223 | + b.commit() |
| 224 | + |
| 225 | +# Resource limits (automatically enables process isolation) |
| 226 | +limits = ResourceLimits(memory=512 * 1024 * 1024, cpu=0.5) |
| 227 | +outcome = BestOfN(scored_task, n=5, resource_limits=limits)(ws) |
| 228 | + |
| 229 | +# All patterns accept resource_limits and group_limits |
| 230 | +outcome = Speculate(candidates, resource_limits=limits, timeout=60)(ws) |
| 231 | +``` |
| 232 | + |
| 233 | +### Result types |
| 234 | + |
| 235 | +```python |
| 236 | +# SpeculationOutcome — returned by all patterns |
| 237 | +outcome.committed # bool — did a winner get committed? |
| 238 | +outcome.winner # SpeculationResult | None |
| 239 | +outcome.all_results # list[SpeculationResult] |
| 240 | + |
| 241 | +# SpeculationResult — per-candidate result |
| 242 | +result.branch_index # int — which candidate |
| 243 | +result.success # bool |
| 244 | +result.score # float |
| 245 | +result.return_value # Any — raw return from the callable |
| 246 | +result.exception # Exception | None |
| 247 | +result.branch_path # Path | None |
| 248 | +``` |
| 249 | + |
| 250 | +## When to use which pattern |
| 251 | + |
| 252 | +| Situation | CLI | Python | |
| 253 | +|-----------|-----|--------| |
| 254 | +| Try one thing safely, rollback on failure | `branching run` | `ws.branch()` | |
| 255 | +| Multiple fix strategies, any success is fine | `branching speculate` | `Speculate` | |
| 256 | +| Same task N times, pick the best result | `branching best-of-n` | `BestOfN` | |
| 257 | +| Iterative fix with error feedback | `branching reflexion` | `Reflexion` | |
| 258 | +| Hierarchical exploration (pick strategy, then refine) | — | `TreeOfThoughts` | |
| 259 | +| Multi-level with multiple survivors per level | — | `BeamSearch` | |
| 260 | +| Pairwise comparison, no absolute scoring | — | `Tournament` | |
0 commit comments