Skip to content

Add interrupt callbacks for terminal tool termination#2209

Draft
malhotra5 wants to merge 1 commit intofeature/conversation-interruptfrom
feature/interrupt-callbacks
Draft

Add interrupt callbacks for terminal tool termination#2209
malhotra5 wants to merge 1 commit intofeature/conversation-interruptfrom
feature/interrupt-callbacks

Conversation

@malhotra5
Copy link
Collaborator

Summary

This PR adds an interrupt callback mechanism that allows conversation.interrupt() to immediately terminate in-flight terminal commands.

Changes

LocalConversation

  • Added _interrupt_callbacks: list attribute to store registered callbacks
  • Added register_interrupt_callback(callback) method to register callbacks
  • Added unregister_interrupt_callback(callback) method to remove callbacks
  • Modified interrupt() to invoke all registered callbacks before changing status

TerminalSession

  • Added _interrupt_requested: bool flag to track interrupt requests
  • Added request_interrupt() method that sets the flag and sends Ctrl+C to the terminal
  • Added _handle_interrupted_command() method to create appropriate observation for interrupted commands
  • Modified execute() to reset interrupt flag at start and check it in the polling loop

TerminalExecutor

  • Modified __call__() to register an interrupt callback when conversation is available
  • The callback calls self.session.request_interrupt()
  • Callback is unregistered in a finally block to ensure cleanup

How it works

When conversation.interrupt() is called while a terminal command is running:

  1. The LLMs are cancelled (existing behavior)
  2. All registered interrupt callbacks are invoked
  3. The terminal executor's callback calls session.request_interrupt()
  4. This sets the interrupt flag and sends Ctrl+C to the process
  5. The terminal session's polling loop detects the flag and returns an "interrupted" observation
  6. The callback is unregistered when the terminal execution completes

Design Decisions

The register_interrupt_callback and unregister_interrupt_callback methods are only on LocalConversation, not on BaseConversation as abstract methods because:

  • RemoteConversation sends interrupt to the server which handles it internally
  • The callback mechanism is specific to local execution
  • Avoids unnecessary no-op implementations

Tests

Added tests for:

  • Callback registration and unregistration
  • Multiple callbacks being invoked
  • Exception handling in callbacks
  • Terminal session interrupt flag behavior

- Add _interrupt_callbacks list to LocalConversation with register/unregister methods
- Invoke all registered callbacks when conversation.interrupt() is called
- Add request_interrupt() method to TerminalSession that sets flag and sends Ctrl+C
- Add interrupt check in TerminalSession execute loop to return early when interrupted
- Register interrupt callback in TerminalExecutor.__call__ to terminate terminal commands
- Add tests for interrupt callback registration and terminal session interruption

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions
Copy link
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/conversation/impl
   local_conversation.py37115358%206, 208–209, 255, 264–265, 289, 294, 304, 322, 329–330, 333–334, 336, 338, 345–346, 349–350, 356–357, 360–361, 364–365, 368, 376–377, 379, 383, 389–392, 396, 400, 406–407, 461, 575, 583, 586–590, 597–598, 601, 610–611, 614, 621, 642, 645, 649–651, 658–659, 662–664, 667, 676, 692, 694, 696, 700, 702–704, 706, 708, 714–715, 728–729, 731, 733, 737–740, 757, 760–762, 766–770, 773–774, 778–781, 803–804, 821–822, 828, 833, 836, 838, 849, 851–853, 873, 876–878, 881, 883, 887, 892, 897, 902–905, 911, 914, 918, 921, 923–925, 927, 945, 947, 961, 965, 973, 989–990, 994, 996, 998, 1028, 1031–1034, 1039–1041, 1047–1048
openhands-tools/openhands/tools/terminal
   impl.py852669%69, 72, 80–81, 108–110, 112–113, 120, 122, 126, 142, 150, 157, 160, 162, 167–168, 171–172, 174, 184, 208–209, 211
openhands-tools/openhands/tools/terminal/terminal
   terminal_session.py2088260%95, 101, 113, 115, 119–121, 143–144, 171, 186–187, 226–228, 233, 236–237, 241, 247, 250, 265–267, 272, 275–276, 280, 286, 289, 303–305, 309, 312–313, 317, 323, 326, 346, 348, 351, 353, 369, 387, 393, 402, 405, 439, 443, 446, 449–450, 456–457, 463, 466, 473–474, 480–481, 540–542, 546, 551, 556–557, 561–562, 565–568, 574–575, 578–580, 585–586, 589
TOTAL19220973449% 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants