"""Example: Parallel tool execution with tool_concurrency_limit. Demonstrates how setting tool_concurrency_limit on an Agent enables concurrent tool execution within a single step. The orchestrator agent delegates to multiple sub-agents in parallel, and each sub-agent itself runs tools concurrently. This stress-tests the parallel execution system end-to-end. """ import json import os import tempfile from collections import defaultdict from pathlib import Path from openhands.sdk import ( LLM, Agent, AgentContext, Conversation, Tool, register_agent, ) from openhands.sdk.context import Skill from openhands.tools.delegate import DelegationVisualizer from openhands.tools.file_editor import FileEditorTool from openhands.tools.task import TaskToolSet from openhands.tools.terminal import TerminalTool 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="parallel-tools-demo", ) # --- Sub-agents --- def create_code_analyst(llm: LLM) -> Agent: """Sub-agent that analyzes code structure.""" return Agent( llm=llm, tools=[ Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name), ], tool_concurrency_limit=4, agent_context=AgentContext( skills=[ Skill( name="code_analysis", content=( "You analyze code structure. Use the terminal to count files, " "lines of code, and list directory structure. Use the file " "editor to read key files. Run multiple commands at once." ), trigger=None, ) ], system_message_suffix="Be concise. Report findings in bullet points.", ), ) def create_doc_reviewer(llm: LLM) -> Agent: """Sub-agent that reviews documentation.""" return Agent( llm=llm, tools=[ Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name), ], tool_concurrency_limit=4, agent_context=AgentContext( skills=[ Skill( name="doc_review", content=( "You review project documentation. Check README files, " "docstrings, and inline comments. Use the terminal and " "file editor to inspect files. Run multiple commands at once." ), trigger=None, ) ], system_message_suffix="Be concise. Report findings in bullet points.", ), ) def create_dependency_checker(llm: LLM) -> Agent: """Sub-agent that checks project dependencies.""" return Agent( llm=llm, tools=[ Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name), ], tool_concurrency_limit=4, agent_context=AgentContext( skills=[ Skill( name="dependency_check", content=( "You analyze project dependencies. Read pyproject.toml, " "requirements files, and package configs. Summarize key " "dependencies, their purposes, and any version constraints. " "Run multiple commands at once." ), trigger=None, ) ], system_message_suffix="Be concise. Report findings in bullet points.", ), ) # Register sub-agents register_agent( name="code_analyst", factory_func=create_code_analyst, description="Analyzes code structure, file counts, and directory layout.", ) register_agent( name="doc_reviewer", factory_func=create_doc_reviewer, description="Reviews documentation quality and completeness.", ) register_agent( name="dependency_checker", factory_func=create_dependency_checker, description="Checks and summarizes project dependencies.", ) # --- Orchestrator agent with parallel execution --- main_agent = Agent( llm=llm, tools=[ Tool(name=TaskToolSet.name), Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name), ], tool_concurrency_limit=8, ) persistence_dir = Path(tempfile.mkdtemp(prefix="parallel_example_")) conversation = Conversation( agent=main_agent, workspace=Path.cwd(), visualizer=DelegationVisualizer(name="Orchestrator"), persistence_dir=persistence_dir, ) print("=" * 80) print("Parallel Tool Execution Stress Test") print("=" * 80) conversation.send_message(""" Analyze the current project by delegating to ALL THREE sub-agents IN PARALLEL: 1. code_analyst: Analyze the project structure (file counts, key directories) 2. doc_reviewer: Review documentation quality (README, docstrings) 3. dependency_checker: Check dependencies (pyproject.toml, requirements) IMPORTANT: Delegate to all three agents at the same time using parallel tool calls. Do NOT delegate one at a time - call all three delegate tools in a single response. Once all three have reported back, write a consolidated summary to project_analysis_report.txt in the working directory. The report should have three sections (Code Structure, Documentation, Dependencies) with the key findings from each sub-agent. """) conversation.run() # --- Analyze persisted events for parallelism --- # # Walk the persistence directory to find all conversations (main + sub-agents). # Each conversation stores events as event-*.json files under an events/ dir. # We parse ActionEvent entries and group by llm_response_id — batches with 2+ # actions sharing the same response ID prove the LLM requested parallel calls # and the executor handled them concurrently. print("\n" + "=" * 80) print("Parallelism Report") print("=" * 80) def _analyze_conversation(events_dir: Path) -> dict[str, list[str]]: """Return {llm_response_id: [tool_name, ...]} for multi-tool batches.""" batches: dict[str, list[str]] = defaultdict(list) for event_file in sorted(events_dir.glob("event-*.json")): data = json.loads(event_file.read_text()) if data.get("kind") == "ActionEvent" and "llm_response_id" in data: batches[data["llm_response_id"]].append(data.get("tool_name", "?")) return {rid: tools for rid, tools in batches.items() if len(tools) >= 2} for events_dir in sorted(persistence_dir.rglob("events")): if not events_dir.is_dir(): continue # Derive a label from the path (main conv vs sub-agent) rel = events_dir.parent.relative_to(persistence_dir) is_subagent = "subagents" in rel.parts label = "sub-agent" if is_subagent else "main agent" multi_batches = _analyze_conversation(events_dir) if multi_batches: for resp_id, tools in multi_batches.items(): print(f"\n {label} batch ({resp_id[:16]}...):") print(f" Parallel tools: {tools}") else: print(f"\n {label}: no parallel batches") cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost print(f"\nTotal cost: ${cost:.4f}") print(f"EXAMPLE_COST: {cost:.4f}")