Coverage for little_loops / fsm / handoff_handler.py: 97%
34 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-22 16:19 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-05-22 16:19 -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
16from little_loops.host_runner import resolve_host
19class HandoffBehavior(Enum):
20 """Behavior when a handoff signal is detected.
22 - TERMINATE: Stop loop execution immediately, no state preservation
23 - PAUSE: Save state with continuation prompt and exit (default)
24 - SPAWN: Save state and spawn a new Claude session to continue
25 """
27 TERMINATE = "terminate"
28 PAUSE = "pause"
29 SPAWN = "spawn"
32@dataclass
33class HandoffResult:
34 """Result from handling a handoff signal.
36 Attributes:
37 behavior: The behavior that was applied
38 continuation_prompt: The continuation prompt from the signal
39 spawned_process: Popen object if spawn behavior was used
40 """
42 behavior: HandoffBehavior
43 continuation_prompt: str | None
44 spawned_process: subprocess.Popen[str] | None = None
47class HandoffHandler:
48 """Handle context handoff signals.
50 Provides configurable behavior for when handoff signals are detected
51 in loop action output.
53 Example:
54 handler = HandoffHandler(HandoffBehavior.PAUSE)
55 result = handler.handle("fix-types", "Continue from iteration 5")
56 # result.behavior == HandoffBehavior.PAUSE
57 # State should be saved by caller
58 """
60 def __init__(self, behavior: HandoffBehavior = HandoffBehavior.PAUSE) -> None:
61 """Initialize handler with behavior.
63 Args:
64 behavior: How to handle handoff signals (default: pause)
65 """
66 self.behavior = behavior
68 def handle(self, loop_name: str, continuation: str | None) -> HandoffResult:
69 """Handle a detected handoff signal.
71 For PAUSE and SPAWN behaviors, the caller (executor) is responsible
72 for saving state with the continuation prompt.
74 Args:
75 loop_name: Name of the loop for spawn commands
76 continuation: Continuation prompt from the signal
78 Returns:
79 HandoffResult with behavior taken and any spawned process
80 """
81 if self.behavior == HandoffBehavior.TERMINATE:
82 return HandoffResult(self.behavior, continuation)
84 if self.behavior == HandoffBehavior.PAUSE:
85 # State saving handled by executor
86 return HandoffResult(self.behavior, continuation)
88 if self.behavior == HandoffBehavior.SPAWN:
89 process = self._spawn_continuation(loop_name, continuation)
90 return HandoffResult(self.behavior, continuation, process)
92 # Should never reach here due to enum exhaustiveness,
93 # but satisfy type checker
94 return HandoffResult(self.behavior, continuation)
96 def _spawn_continuation(
97 self, loop_name: str, continuation: str | None
98 ) -> subprocess.Popen[str]:
99 """Spawn new Claude session to continue loop.
101 Creates a new Claude CLI process with a prompt instructing it
102 to resume the loop execution.
104 Args:
105 loop_name: Name of the loop to resume
106 continuation: Continuation context from handoff
108 Returns:
109 Popen object for the spawned process
110 """
111 prompt_parts = [f"Continue loop execution. Run: ll-loop resume {loop_name}"]
112 if continuation:
113 prompt_parts.append(f"\n\n{continuation}")
114 prompt = "".join(prompt_parts)
116 invocation = resolve_host().build_detached(prompt=prompt)
117 # Legacy argv had no perm-skip; strip it for no-behavior-change refactor.
118 args = [a for a in invocation.args if a != "--dangerously-skip-permissions"]
119 return subprocess.Popen(
120 [invocation.binary, *args],
121 text=True,
122 start_new_session=True,
123 stdout=subprocess.DEVNULL,
124 stderr=subprocess.DEVNULL,
125 stdin=subprocess.DEVNULL,
126 )