Coverage for little_loops / fsm / handoff_handler.py: 53%
32 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-03-18 16:18 -0500
1"""Handoff handling for FSM loop execution.
3This module provides behavior handlers for context handoff signals,
4supporting pause, spawn, and terminate behaviors.
6The handler is Claude-specific and knows how to spawn continuation sessions
7via the Claude CLI.
8"""
10from __future__ import annotations
12import subprocess
13from dataclasses import dataclass
14from enum import Enum
17class HandoffBehavior(Enum):
18 """Behavior when a handoff signal is detected.
20 - TERMINATE: Stop loop execution immediately, no state preservation
21 - PAUSE: Save state with continuation prompt and exit (default)
22 - SPAWN: Save state and spawn a new Claude session to continue
23 """
25 TERMINATE = "terminate"
26 PAUSE = "pause"
27 SPAWN = "spawn"
30@dataclass
31class HandoffResult:
32 """Result from handling a handoff signal.
34 Attributes:
35 behavior: The behavior that was applied
36 continuation_prompt: The continuation prompt from the signal
37 spawned_process: Popen object if spawn behavior was used
38 """
40 behavior: HandoffBehavior
41 continuation_prompt: str | None
42 spawned_process: subprocess.Popen[str] | None = None
45class HandoffHandler:
46 """Handle context handoff signals.
48 Provides configurable behavior for when handoff signals are detected
49 in loop action output.
51 Example:
52 handler = HandoffHandler(HandoffBehavior.PAUSE)
53 result = handler.handle("fix-types", "Continue from iteration 5")
54 # result.behavior == HandoffBehavior.PAUSE
55 # State should be saved by caller
56 """
58 def __init__(self, behavior: HandoffBehavior = HandoffBehavior.PAUSE) -> None:
59 """Initialize handler with behavior.
61 Args:
62 behavior: How to handle handoff signals (default: pause)
63 """
64 self.behavior = behavior
66 def handle(self, loop_name: str, continuation: str | None) -> HandoffResult:
67 """Handle a detected handoff signal.
69 For PAUSE and SPAWN behaviors, the caller (executor) is responsible
70 for saving state with the continuation prompt.
72 Args:
73 loop_name: Name of the loop for spawn commands
74 continuation: Continuation prompt from the signal
76 Returns:
77 HandoffResult with behavior taken and any spawned process
78 """
79 if self.behavior == HandoffBehavior.TERMINATE:
80 return HandoffResult(self.behavior, continuation)
82 if self.behavior == HandoffBehavior.PAUSE:
83 # State saving handled by executor
84 return HandoffResult(self.behavior, continuation)
86 if self.behavior == HandoffBehavior.SPAWN:
87 process = self._spawn_continuation(loop_name, continuation)
88 return HandoffResult(self.behavior, continuation, process)
90 # Should never reach here due to enum exhaustiveness,
91 # but satisfy type checker
92 return HandoffResult(self.behavior, continuation)
94 def _spawn_continuation(
95 self, loop_name: str, continuation: str | None
96 ) -> subprocess.Popen[str]:
97 """Spawn new Claude session to continue loop.
99 Creates a new Claude CLI process with a prompt instructing it
100 to resume the loop execution.
102 Args:
103 loop_name: Name of the loop to resume
104 continuation: Continuation context from handoff
106 Returns:
107 Popen object for the spawned process
108 """
109 prompt_parts = [f"Continue loop execution. Run: ll-loop resume {loop_name}"]
110 if continuation:
111 prompt_parts.append(f"\n\n{continuation}")
112 prompt = "".join(prompt_parts)
114 cmd = ["claude", "-p", prompt]
115 return subprocess.Popen(
116 cmd,
117 text=True,
118 start_new_session=True,
119 stdout=subprocess.DEVNULL,
120 stderr=subprocess.DEVNULL,
121 stdin=subprocess.DEVNULL,
122 )