from prompt_toolkit import HTML, print_formatted_text
from openhands.sdk import BaseConversation, Message
from openhands.sdk.conversation.state import (
ConversationExecutionStatus,
ConversationState,
)
from openhands.sdk.security.confirmation_policy import (
AlwaysConfirm,
ConfirmationPolicyBase,
ConfirmRisky,
NeverConfirm,
)
from openhands_cli.listeners.pause_listener import PauseListener, pause_listener
from openhands_cli.user_actions import ask_user_confirmation
from openhands_cli.user_actions.types import UserConfirmation
from openhands_cli.setup import setup_conversation
class ConversationRunner:
"""Handles the conversation state machine logic cleanly."""
def __init__(self, conversation: BaseConversation):
self.conversation = conversation
@property
def is_confirmation_mode_active(self):
return self.conversation.is_confirmation_mode_active
def toggle_confirmation_mode(self):
new_confirmation_mode_state = not self.is_confirmation_mode_active
self.conversation = setup_conversation(
self.conversation.id,
include_security_analyzer=new_confirmation_mode_state
)
if new_confirmation_mode_state:
# Enable confirmation mode: set AlwaysConfirm policy
self.set_confirmation_policy(AlwaysConfirm())
else:
# Disable confirmation mode: set NeverConfirm policy and remove security analyzer
self.set_confirmation_policy(NeverConfirm())
def set_confirmation_policy(
self, confirmation_policy: ConfirmationPolicyBase
) -> None:
self.conversation.set_confirmation_policy(confirmation_policy)
def _start_listener(self) -> None:
self.listener = PauseListener(on_pause=self.conversation.pause)
self.listener.start()
def _print_run_status(self) -> None:
print_formatted_text('')
if (
self.conversation.state.execution_status
== ConversationExecutionStatus.PAUSED
):
print_formatted_text(
HTML(
'Resuming paused conversation... (Press Ctrl-P to pause)'
)
)
else:
print_formatted_text(
HTML(
'Agent running... (Press Ctrl-P to pause)'
)
)
print_formatted_text('')
def process_message(self, message: Message | None) -> None:
"""Process a user message through the conversation.
Args:
message: The user message to process
"""
self._print_run_status()
# Send message to conversation
if message:
self.conversation.send_message(message)
if self.is_confirmation_mode_active:
self._run_with_confirmation()
else:
self._run_without_confirmation()
def _run_without_confirmation(self) -> None:
with pause_listener(self.conversation):
self.conversation.run()
def _run_with_confirmation(self) -> None:
# If agent was paused, resume with confirmation request
if (
self.conversation.state.execution_status
== ConversationExecutionStatus.WAITING_FOR_CONFIRMATION
):
user_confirmation = self._handle_confirmation_request()
if user_confirmation == UserConfirmation.DEFER:
return
while True:
with pause_listener(self.conversation) as listener:
self.conversation.run()
if listener.is_paused():
break
# In confirmation mode, agent either finishes or waits for user confirmation
if (
self.conversation.state.execution_status
== ConversationExecutionStatus.FINISHED
):
break
elif (
self.conversation.state.execution_status
== ConversationExecutionStatus.WAITING_FOR_CONFIRMATION
):
user_confirmation = self._handle_confirmation_request()
if user_confirmation == UserConfirmation.DEFER:
return
else:
raise Exception('Infinite loop')
def _handle_confirmation_request(self) -> UserConfirmation:
"""Handle confirmation request from user.
Returns:
UserConfirmation indicating the user's choice
"""
pending_actions = ConversationState.get_unmatched_actions(
self.conversation.state.events
)
if not pending_actions:
return UserConfirmation.ACCEPT
result = ask_user_confirmation(
pending_actions,
isinstance(self.conversation.state.confirmation_policy, ConfirmRisky),
)
decision = result.decision
policy_change = result.policy_change
if decision == UserConfirmation.REJECT:
self.conversation.reject_pending_actions(
result.reason or 'User rejected the actions'
)
return decision
if decision == UserConfirmation.DEFER:
self.conversation.pause()
return decision
if isinstance(policy_change, NeverConfirm):
print_formatted_text(
HTML(
'Confirmation mode disabled. Agent will proceed without asking.'
)
)
# Remove security analyzer when policy is never confirm
self.toggle_confirmation_mode()
return decision
if isinstance(policy_change, ConfirmRisky):
print_formatted_text(
HTML(
'Security-based confirmation enabled. '
'LOW/MEDIUM risk actions will auto-confirm, HIGH risk actions will ask for confirmation.'
)
)
# Keep security analyzer, change existing policy
self.set_confirmation_policy(policy_change)
return decision
# Accept action without changing existing policies
assert decision == UserConfirmation.ACCEPT
return decision