Coverage for little_loops / fsm / compilers.py: 100%

100 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-03-04 01:17 -0600

1"""Paradigm compilers for FSM loop generation. 

2 

3Each paradigm (goal, convergence, invariants, imperative) compiles to the 

4universal FSM schema via deterministic template expansion. This provides 

5a simple authoring experience while maintaining a single execution engine. 

6 

7Compilers transform high-level paradigm YAML specifications into FSMLoop 

8instances that can be validated and executed. Each compiler is a pure 

9function (~50-100 lines) that performs template expansion with variable 

10substitution. 

11 

12Example usage: 

13 >>> spec = { 

14 ... "paradigm": "goal", 

15 ... "goal": "No type errors in src/", 

16 ... "tools": ["/ll:check-code types", "/ll:manage-issue bug fix"], 

17 ... } 

18 >>> fsm = compile_paradigm(spec) 

19 >>> fsm.initial 

20 'evaluate' 

21""" 

22 

23from __future__ import annotations 

24 

25import re 

26from typing import Any 

27 

28from little_loops.fsm.schema import ( 

29 EvaluateConfig, 

30 FSMLoop, 

31 RouteConfig, 

32 StateConfig, 

33) 

34 

35 

36def _slugify(text: str, max_length: int = 50) -> str: 

37 """Convert text to slug format for FSM names. 

38 

39 Args: 

40 text: Text to convert to slug 

41 max_length: Maximum length of the resulting slug 

42 

43 Returns: 

44 Lowercase slug with hyphens, truncated to max_length 

45 """ 

46 text = re.sub(r"[^\w\s-]", "", text) 

47 text = re.sub(r"[-\s]+", "-", text) 

48 return text.strip("-").lower()[:max_length] 

49 

50 

51def _build_evaluate_config(evaluator_spec: dict[str, Any] | None) -> EvaluateConfig | None: 

52 """Build EvaluateConfig from evaluator specification dict. 

53 

54 Args: 

55 evaluator_spec: Optional evaluator configuration dict with keys: 

56 - type: Evaluator type (exit_code, output_contains, output_numeric, llm_structured) 

57 - pattern: Pattern string for output_contains 

58 - operator: Comparison operator for output_numeric (eq, lt, gt, le, ge, ne) 

59 - target: Target value for output_numeric 

60 

61 Returns: 

62 EvaluateConfig instance if spec provided, None otherwise 

63 """ 

64 if evaluator_spec is None: 

65 return None 

66 

67 eval_type = evaluator_spec.get("type", "exit_code") 

68 

69 # For exit_code, we can return None to use the default behavior 

70 if eval_type == "exit_code": 

71 return None 

72 

73 return EvaluateConfig( 

74 type=eval_type, 

75 pattern=evaluator_spec.get("pattern"), 

76 operator=evaluator_spec.get("operator"), 

77 target=evaluator_spec.get("target"), 

78 ) 

79 

80 

81def compile_paradigm(spec: dict[str, Any]) -> FSMLoop: 

82 """Route to appropriate compiler based on paradigm field. 

83 

84 This is the main entry point for paradigm compilation. It examines 

85 the 'paradigm' field in the spec and routes to the appropriate 

86 compiler function. 

87 

88 Args: 

89 spec: Paradigm specification dictionary. Must include a 'paradigm' 

90 field (defaults to 'fsm' if not present). 

91 

92 Returns: 

93 Compiled FSMLoop instance 

94 

95 Raises: 

96 ValueError: If the paradigm is unknown 

97 

98 Example: 

99 >>> spec = {"paradigm": "goal", "goal": "Clean", "tools": ["cmd"]} 

100 >>> fsm = compile_paradigm(spec) 

101 >>> fsm.paradigm 

102 'goal' 

103 """ 

104 paradigm = spec.get("paradigm", "fsm") 

105 

106 compilers = { 

107 "goal": compile_goal, 

108 "convergence": compile_convergence, 

109 "invariants": compile_invariants, 

110 "imperative": compile_imperative, 

111 "fsm": _passthrough_fsm, 

112 } 

113 

114 if paradigm not in compilers: 

115 raise ValueError( 

116 f"Unknown paradigm: '{paradigm}'. Must be one of: {', '.join(sorted(compilers.keys()))}" 

117 ) 

118 

119 return compilers[paradigm](spec) 

120 

121 

122def _passthrough_fsm(spec: dict[str, Any]) -> FSMLoop: 

123 """Pass through FSM spec directly (no compilation needed). 

124 

125 Args: 

126 spec: FSM specification dictionary matching FSMLoop schema 

127 

128 Returns: 

129 FSMLoop instance created from the spec 

130 """ 

131 return FSMLoop.from_dict(spec) 

132 

133 

134def compile_goal(spec: dict[str, Any]) -> FSMLoop: 

135 """Compile goal paradigm to FSM. 

136 

137 Goal paradigm: evaluate → (success → done, failure → fix), fix → evaluate 

138 

139 The goal paradigm is the simplest: it repeatedly checks a condition 

140 and applies a fix until the goal is achieved. 

141 

142 Input spec: 

143 paradigm: goal 

144 goal: "No type errors in src/" 

145 tools: 

146 - /ll:check-code types # Check tool (first) 

147 - /ll:manage-issue bug fix # Fix tool (second, optional) 

148 max_iterations: 50 # Optional, defaults to 50 (examples show 20 for brevity) 

149 name: "my-goal" # Optional, auto-generated from goal 

150 evaluator: # Optional evaluator config 

151 type: output_contains 

152 pattern: "Success" 

153 

154 Args: 

155 spec: Goal paradigm specification dict 

156 

157 Returns: 

158 Compiled FSMLoop instance with evaluate/fix/done states 

159 

160 Raises: 

161 ValueError: If required fields are missing 

162 """ 

163 # Validate required fields 

164 required = ["goal", "tools"] 

165 missing = [f for f in required if f not in spec] 

166 if missing: 

167 raise ValueError(f"Goal paradigm requires: {', '.join(missing)}") 

168 

169 if "tools" in spec and not spec["tools"]: 

170 raise ValueError("Goal paradigm 'tools' requires at least one tool") 

171 

172 goal = spec["goal"] 

173 tools = spec["tools"] 

174 check_tool = tools[0] 

175 fix_tool = tools[1] if len(tools) > 1 else tools[0] 

176 

177 name = spec.get("name", f"goal-{_slugify(goal)}") 

178 

179 # Extract evaluator config if provided 

180 evaluate_config = _build_evaluate_config(spec.get("evaluator")) 

181 

182 states = { 

183 "evaluate": StateConfig( 

184 action=check_tool, 

185 evaluate=evaluate_config, 

186 on_success="done", 

187 on_failure="fix", 

188 on_error="fix", 

189 ), 

190 "fix": StateConfig( 

191 action=fix_tool, 

192 next="evaluate", 

193 ), 

194 "done": StateConfig(terminal=True), 

195 } 

196 

197 return FSMLoop( 

198 name=name, 

199 paradigm="goal", 

200 initial="evaluate", 

201 states=states, 

202 max_iterations=spec.get("max_iterations", 50), 

203 backoff=spec.get("backoff"), 

204 timeout=spec.get("timeout"), 

205 on_handoff=spec.get("on_handoff", "pause"), 

206 ) 

207 

208 

209def compile_convergence(spec: dict[str, Any]) -> FSMLoop: 

210 """Compile convergence paradigm to FSM. 

211 

212 Convergence paradigm: measure → (target → done, progress → apply, stall → done), 

213 apply → measure 

214 

215 The convergence paradigm drives a metric toward a target value. It measures 

216 the current value, compares to the previous measurement, and routes based 

217 on whether progress is being made. 

218 

219 Input spec: 

220 paradigm: convergence 

221 name: "reduce-lint-errors" 

222 check: "ruff check src/ --output-format=json | jq '.count'" 

223 toward: 0 

224 using: "/ll:check-code fix" 

225 tolerance: 0 # Optional, defaults to 0 

226 

227 Args: 

228 spec: Convergence paradigm specification dict 

229 

230 Returns: 

231 Compiled FSMLoop instance with measure/apply/done states 

232 

233 Raises: 

234 ValueError: If required fields are missing 

235 """ 

236 # Validate required fields 

237 required = ["name", "check", "toward", "using"] 

238 missing = [f for f in required if f not in spec] 

239 if missing: 

240 raise ValueError(f"Convergence paradigm requires: {', '.join(missing)}") 

241 

242 name = spec["name"] 

243 metric_cmd = spec["check"] 

244 target = spec["toward"] 

245 fix_action = spec["using"] 

246 tolerance = spec.get("tolerance", 0) 

247 

248 # Build context for variable interpolation 

249 context = { 

250 "metric_cmd": metric_cmd, 

251 "target": target, 

252 "tolerance": tolerance, 

253 } 

254 

255 states = { 

256 "measure": StateConfig( 

257 action="${context.metric_cmd}", 

258 capture="current_value", 

259 evaluate=EvaluateConfig( 

260 type="convergence", 

261 target="${context.target}", 

262 tolerance="${context.tolerance}", 

263 previous="${prev.output}", 

264 ), 

265 route=RouteConfig( 

266 routes={ 

267 "target": "done", 

268 "progress": "apply", 

269 "stall": "done", 

270 } 

271 ), 

272 ), 

273 "apply": StateConfig( 

274 action=fix_action, 

275 next="measure", 

276 ), 

277 "done": StateConfig(terminal=True), 

278 } 

279 

280 return FSMLoop( 

281 name=name, 

282 paradigm="convergence", 

283 initial="measure", 

284 states=states, 

285 context=context, 

286 max_iterations=spec.get("max_iterations", 50), 

287 backoff=spec.get("backoff"), 

288 timeout=spec.get("timeout"), 

289 ) 

290 

291 

292def compile_invariants(spec: dict[str, Any]) -> FSMLoop: 

293 """Compile invariants paradigm to FSM. 

294 

295 Invariants paradigm: check_1 → (success → check_2, failure → fix_1), 

296 fix_1 → check_1, ... 

297 

298 The invariants paradigm chains multiple constraints. Each constraint 

299 is checked in sequence; if any fails, its fix is applied before 

300 re-checking. When all pass, the loop can optionally restart (maintain mode). 

301 

302 Input spec: 

303 paradigm: invariants 

304 name: "code-quality-guardian" 

305 constraints: 

306 - name: "tests-pass" 

307 check: "pytest" 

308 fix: "/ll:manage-issue bug fix" 

309 evaluator: # Optional per-constraint 

310 type: output_contains 

311 pattern: "passed" 

312 - name: "lint-clean" 

313 check: "ruff check src/" 

314 fix: "/ll:check-code fix" 

315 maintain: true # Optional, restarts after all valid 

316 

317 Args: 

318 spec: Invariants paradigm specification dict 

319 

320 Returns: 

321 Compiled FSMLoop instance with check/fix pairs and all_valid terminal 

322 

323 Raises: 

324 ValueError: If required fields are missing or constraint is invalid 

325 """ 

326 # Validate required fields 

327 required = ["name", "constraints"] 

328 missing = [f for f in required if f not in spec] 

329 if missing: 

330 raise ValueError(f"Invariants paradigm requires: {', '.join(missing)}") 

331 

332 if "constraints" in spec and not spec["constraints"]: 

333 raise ValueError("Invariants paradigm 'constraints' requires at least one constraint") 

334 

335 name = spec["name"] 

336 constraints = spec["constraints"] 

337 maintain = spec.get("maintain", False) 

338 

339 # Validate each constraint 

340 for i, constraint in enumerate(constraints): 

341 if "name" not in constraint: 

342 raise ValueError(f"Constraint {i} requires 'name' field") 

343 if "check" not in constraint: 

344 raise ValueError(f"Constraint '{constraint.get('name', i)}' requires 'check' field") 

345 if "fix" not in constraint: 

346 raise ValueError(f"Constraint '{constraint.get('name', i)}' requires 'fix' field") 

347 

348 states: dict[str, StateConfig] = {} 

349 

350 for i, constraint in enumerate(constraints): 

351 check_state = f"check_{constraint['name']}" 

352 fix_state = f"fix_{constraint['name']}" 

353 

354 # Next check state or terminal 

355 next_check = ( 

356 f"check_{constraints[i + 1]['name']}" if i + 1 < len(constraints) else "all_valid" 

357 ) 

358 

359 # Extract evaluator config if provided for this constraint 

360 evaluate_config = _build_evaluate_config(constraint.get("evaluator")) 

361 

362 states[check_state] = StateConfig( 

363 action=constraint["check"], 

364 evaluate=evaluate_config, 

365 on_success=next_check, 

366 on_failure=fix_state, 

367 ) 

368 states[fix_state] = StateConfig( 

369 action=constraint["fix"], 

370 next=check_state, 

371 ) 

372 

373 # Terminal state with optional maintain loop-back 

374 first_check = f"check_{constraints[0]['name']}" 

375 states["all_valid"] = StateConfig( 

376 terminal=True, 

377 on_maintain=first_check if maintain else None, 

378 ) 

379 

380 return FSMLoop( 

381 name=name, 

382 paradigm="invariants", 

383 initial=first_check, 

384 states=states, 

385 maintain=maintain, 

386 max_iterations=spec.get("max_iterations", 50), 

387 backoff=spec.get("backoff"), 

388 timeout=spec.get("timeout"), 

389 ) 

390 

391 

392def compile_imperative(spec: dict[str, Any]) -> FSMLoop: 

393 """Compile imperative paradigm to FSM. 

394 

395 Imperative paradigm: step_0 → step_1 → ... → check_done → 

396 (success → done, failure → step_0) 

397 

398 The imperative paradigm runs a sequence of steps, then checks an 

399 exit condition. If the condition fails, the sequence restarts from 

400 the beginning. 

401 

402 Input spec: 

403 paradigm: imperative 

404 name: "fix-all-types" 

405 steps: 

406 - /ll:check-code types 

407 - /ll:manage-issue bug fix 

408 until: 

409 check: "mypy src/" 

410 passes: true 

411 evaluator: # Optional evaluator for exit condition 

412 type: output_contains 

413 pattern: "Success" 

414 max_iterations: 50 # Optional, defaults to 50 (examples show 20 for brevity) 

415 backoff: 2 # Seconds between iterations 

416 

417 Args: 

418 spec: Imperative paradigm specification dict 

419 

420 Returns: 

421 Compiled FSMLoop instance with step sequence and check_done 

422 

423 Raises: 

424 ValueError: If required fields are missing 

425 """ 

426 # Validate required fields 

427 required = ["name", "steps", "until"] 

428 missing = [f for f in required if f not in spec] 

429 if missing: 

430 raise ValueError(f"Imperative paradigm requires: {', '.join(missing)}") 

431 

432 if "steps" in spec and not spec["steps"]: 

433 raise ValueError("Imperative paradigm 'steps' requires at least one step") 

434 

435 if "check" not in spec["until"]: 

436 raise ValueError("Imperative paradigm 'until' requires 'check' field") 

437 

438 name = spec["name"] 

439 steps = spec["steps"] 

440 until_check = spec["until"]["check"] 

441 

442 # Extract evaluator config for exit condition if provided 

443 evaluate_config = _build_evaluate_config(spec["until"].get("evaluator")) 

444 

445 states: dict[str, StateConfig] = {} 

446 

447 # Create step states 

448 for i, step in enumerate(steps): 

449 state_name = f"step_{i}" 

450 next_state = f"step_{i + 1}" if i + 1 < len(steps) else "check_done" 

451 

452 states[state_name] = StateConfig( 

453 action=step, 

454 next=next_state, 

455 ) 

456 

457 # Create check_done state with optional evaluator 

458 states["check_done"] = StateConfig( 

459 action=until_check, 

460 evaluate=evaluate_config, 

461 on_success="done", 

462 on_failure="step_0", 

463 ) 

464 

465 # Terminal state 

466 states["done"] = StateConfig(terminal=True) 

467 

468 return FSMLoop( 

469 name=name, 

470 paradigm="imperative", 

471 initial="step_0", 

472 states=states, 

473 max_iterations=spec.get("max_iterations", 50), 

474 backoff=spec.get("backoff"), 

475 timeout=spec.get("timeout"), 

476 )