From 2e18ad594a29af975c1295acb2cb856d603fbb75 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 26 Feb 2026 17:45:25 +0000 Subject: [PATCH 1/2] Add documentation for conversation.interrupt() feature Documents the new interrupt() method that immediately cancels in-flight LLM calls, as opposed to pause() which waits for completion. Co-authored-by: openhands --- docs.json | 1 + sdk/guides/convo-interrupt.mdx | 238 +++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 sdk/guides/convo-interrupt.mdx diff --git a/docs.json b/docs.json index 8233742c..3cdfb880 100644 --- a/docs.json +++ b/docs.json @@ -324,6 +324,7 @@ "group": "Conversation Features", "pages": [ "sdk/guides/convo-pause-and-resume", + "sdk/guides/convo-interrupt", "sdk/guides/convo-custom-visualizer", "sdk/guides/convo-send-message-while-running", "sdk/guides/convo-async", diff --git a/sdk/guides/convo-interrupt.mdx b/sdk/guides/convo-interrupt.mdx new file mode 100644 index 00000000..17bc1461 --- /dev/null +++ b/sdk/guides/convo-interrupt.mdx @@ -0,0 +1,238 @@ +--- +title: Interrupt Agent +description: Immediately cancel in-flight LLM calls to stop agent execution. +--- + +import RunExampleCode from "/sdk/shared-snippets/how-to-run-example.mdx"; + +> A ready-to-run example is available [here](#ready-to-run-example)! + +### Interrupting vs Pausing + +While `conversation.pause()` waits for the current LLM call to complete before stopping, +`conversation.interrupt()` immediately cancels any in-flight LLM call: + +| Method | Behavior | +|--------|----------| +| `pause()` | Waits for current LLM call to finish, then stops | +| `interrupt()` | Immediately cancels the LLM call and stops | + +Use `interrupt()` when you need to: +- Stop expensive reasoning tasks immediately +- Save costs by cancelling long-running API calls +- Provide responsive Ctrl+C handling in interactive applications + +### Basic Usage + +```python icon="python" focus={5} wrap +# In a signal handler or another thread: +def signal_handler(signum, frame): + print("Interrupting agent...") + conversation.interrupt() + +signal.signal(signal.SIGINT, signal_handler) +``` + +### How It Works + +When you call `interrupt()`: +1. The async task running the LLM call is cancelled +2. The HTTP connection is closed immediately +3. `LLMCancelledError` is raised internally +4. The conversation status changes to `paused` + +After interruption, you can: +- Send new messages with `conversation.send_message()` +- Resume with `conversation.run()` +- Check the conversation history for partial results + +## Ready-to-run Example + + +This example is available on GitHub: [examples/01_standalone_sdk/43_interrupt_example.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/43_interrupt_example.py) + + +This example demonstrates interrupting a long reasoning task with Ctrl+C: + +```python icon="python" expandable examples/01_standalone_sdk/43_interrupt_example.py +"""Example: Interrupting agent execution with Ctrl+C. + +This example demonstrates how to use conversation.interrupt() to immediately +cancel an in-flight LLM call when the user presses Ctrl+C. + +Unlike pause(), which waits for the current LLM call to complete, +interrupt() cancels the call immediately by: +- Cancelling the async task running the LLM call +- Closing the HTTP connection +- Raising LLMCancelledError + +This is useful for: +- Long-running reasoning tasks that you want to stop immediately +- Expensive API calls you want to cancel to save costs +- Interactive applications where responsiveness is important + +Usage: + LLM_API_KEY=your_key python 43_interrupt_example.py + +Press Ctrl+C at any time to interrupt the agent. +""" + +import os +import signal +import sys +import threading +import time + +from openhands.sdk import LLM, Agent, Conversation, Tool +from openhands.tools.terminal import TerminalTool + + +PROMPT = """ +I need you to solve this complex logic puzzle step by step, showing your reasoning: + +There are 5 houses in a row, each a different color (Red, Green, Blue, Yellow, White). +Each house is occupied by a person of different nationality. +Each person has a different pet, drink, and cigarette brand. + +Clues: +1. The British person lives in the red house. +2. The Swedish person keeps dogs as pets. +3. The Danish person drinks tea. +4. The green house is on the left of the white house. +5. The green house's owner drinks coffee. +6. The person who smokes Pall Mall rears birds. +7. The owner of the yellow house smokes Dunhill. +8. The person living in the center house drinks milk. +9. The Norwegian lives in the first house. +10. The person who smokes Blend lives next to the one who keeps cats. +11. The person who keeps horses lives next to the one who smokes Dunhill. +12. The person who smokes Blue Master drinks beer. +13. The German smokes Prince. +14. The Norwegian lives next to the blue house. +15. The person who smokes Blend has a neighbor who drinks water. + +Question: Who owns the fish? + +Please solve this completely, showing your full reasoning process with all deductions. +After solving, create a file called 'puzzle_solution.txt' with your complete solution. +""" + + +def main(): + # Track timing + start_time: float | None = None + interrupt_time: float | None = None + + # Configure LLM - use gpt-5.2 for long reasoning tasks + # Falls back to environment variable model if gpt-5.2 not available + api_key = os.getenv("LLM_API_KEY") + if not api_key: + print("Error: LLM_API_KEY environment variable is not set.") + sys.exit(1) + + model = os.getenv("LLM_MODEL", "openai/gpt-5.2") + base_url = os.getenv("LLM_BASE_URL") + + print("=" * 70) + print("Interrupt Example - Press Ctrl+C to immediately stop the agent") + print("=" * 70) + print() + + llm = LLM( + usage_id="reasoning-agent", + model=model, + base_url=base_url, + api_key=api_key, + ) + + print(f"Using model: {model}") + print() + + # Create agent with minimal tools + agent = Agent( + llm=llm, + tools=[Tool(name=TerminalTool.name)], + ) + + conversation = Conversation(agent=agent, workspace=os.getcwd()) + + # Set up Ctrl+C handler + def signal_handler(_signum, _frame): + nonlocal interrupt_time + interrupt_time = time.time() + print("\n") + print("=" * 70) + print("Ctrl+C detected! Interrupting agent...") + print("=" * 70) + + # Call interrupt() - this immediately cancels any in-flight LLM call + conversation.interrupt() + + signal.signal(signal.SIGINT, signal_handler) + + # Send a task that requires long reasoning + print("Sending a complex reasoning task to the agent...") + print("(This task is designed to take a while - press Ctrl+C to interrupt)") + print() + + conversation.send_message(PROMPT) + print(f"Agent status: {conversation.state.execution_status}") + print() + + # Run in background thread so we can handle signals + def run_agent(): + conversation.run() + + start_time = time.time() + thread = threading.Thread(target=run_agent) + thread.start() + + print("Agent is working... (press Ctrl+C to interrupt)") + print() + + # Wait for thread to complete (either normally or via interrupt) + thread.join() + + end_time = time.time() + + # Report timing + print() + print("=" * 70) + print("Results") + print("=" * 70) + print() + print(f"Final status: {conversation.state.execution_status}") + print() + + if interrupt_time: + interrupt_latency = end_time - interrupt_time + total_time = end_time - start_time + print(f"Total time from start to stop: {total_time:.2f} seconds") + print(f"Time from Ctrl+C to full stop: {interrupt_latency:.3f} seconds") + print() + print("The agent was interrupted immediately!") + print("Without interrupt(), you would have had to wait for the full") + print("LLM response to complete before the agent would stop.") + else: + total_time = end_time - start_time + print(f"Total time: {total_time:.2f} seconds") + print("Agent completed normally (was not interrupted)") + + print() + + # Report cost + cost = llm.metrics.accumulated_cost + print(f"Accumulated cost: ${cost:.6f}") + print(f"EXAMPLE_COST: {cost}") + + +if __name__ == "__main__": + main() +``` + + + +## Next Steps + +- **[Pause and Resume](/sdk/guides/convo-pause-and-resume)** - Gracefully pause without cancelling LLM calls +- **[Send Message While Processing](/sdk/guides/convo-send-message-while-running)** - Queue messages during execution From f7e2678fcebbaf2acf3b86037ce4689ebbf9b4ff Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 26 Feb 2026 17:47:43 +0000 Subject: [PATCH 2/2] Add AgentDefinition example to delegation guide Documents the 42_file_based_subagents.py example which shows how to define sub-agents declaratively using AgentDefinition instead of factory functions. Co-authored-by: openhands --- sdk/guides/agent-delegation.mdx | 82 +++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/sdk/guides/agent-delegation.mdx b/sdk/guides/agent-delegation.mdx index c368a178..8dc01dc4 100644 --- a/sdk/guides/agent-delegation.mdx +++ b/sdk/guides/agent-delegation.mdx @@ -359,3 +359,85 @@ print(f"EXAMPLE_COST: {cost_1 + cost_2}") ``` + +## Using AgentDefinition for Declarative Sub-Agents + +For simpler use cases, you can define sub-agents declaratively using `AgentDefinition` instead of writing factory functions: + + +This example is available on GitHub: [examples/01_standalone_sdk/42_file_based_subagents.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/42_file_based_subagents.py) + + +```python icon="python" expandable examples/01_standalone_sdk/42_file_based_subagents.py +"""Example: Defining a sub-agent inline with AgentDefinition. + +Defines a grammar-checker sub-agent using AgentDefinition, registers it, +and delegates work to it from an orchestrator agent. The orchestrator then +asks the builtin default agent to judge the results. +""" + +import os +from pathlib import Path + +from openhands.sdk import ( + LLM, + Agent, + Conversation, + Tool, + agent_definition_to_factory, + register_agent, +) +from openhands.sdk.subagent import AgentDefinition +from openhands.sdk.tool import register_tool +from openhands.tools.delegate import DelegateTool, DelegationVisualizer + + +# 1. Define a sub-agent using AgentDefinition +grammar_checker = AgentDefinition( + name="grammar-checker", + description="Checks documents for grammatical errors.", + tools=["file_editor"], + system_prompt="You are a grammar expert. Find and list grammatical errors.", +) + +# 2. Register it in the delegate registry +register_agent( + name=grammar_checker.name, + factory_func=agent_definition_to_factory(grammar_checker), + description=grammar_checker.description, +) + +# 3. Set up the orchestrator agent with the DelegateTool +llm = LLM( + model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"), + api_key=os.getenv("LLM_API_KEY"), + base_url=os.getenv("LLM_BASE_URL"), + usage_id="file-agents-demo", +) + +register_tool("DelegateTool", DelegateTool) +main_agent = Agent( + llm=llm, + tools=[Tool(name="DelegateTool")], +) +conversation = Conversation( + agent=main_agent, + workspace=Path.cwd(), + visualizer=DelegationVisualizer(name="Orchestrator"), +) + +# 4. Ask the orchestrator to delegate to our agent +task = ( + "Please delegate to the grammar-checker agent and ask it to review " + "the README.md file in search of grammatical errors.\n" + "Then ask the default agent to judge the errors." +) +conversation.send_message(task) +conversation.run() + +cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost +print(f"\nTotal cost: ${cost:.4f}") +print(f"EXAMPLE_COST: {cost:.4f}") +``` + +