Skip to content

Commit 9d4a7a4

Browse files
feat(delegate): Add create_sub_visualizer method for custom sub-agent visualization (#1767)
Co-authored-by: openhands <[email protected]>
1 parent b471909 commit 9d4a7a4

File tree

5 files changed

+95
-12
lines changed

5 files changed

+95
-12
lines changed

openhands-sdk/openhands/sdk/conversation/visualizer/base.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,26 @@ def on_event(self, event: Event) -> None:
6565
event: The event to visualize
6666
"""
6767
pass
68+
69+
def create_sub_visualizer(
70+
self,
71+
agent_id: str, # noqa: ARG002
72+
) -> "ConversationVisualizerBase | None":
73+
"""Create a visualizer for a sub-agent during delegation.
74+
75+
Override this method to support sub-agent visualization in multi-agent
76+
delegation scenarios. The sub-visualizer will be used to display events
77+
from the spawned sub-agent.
78+
79+
By default, returns None which means sub-agents will not have visualization.
80+
Subclasses that support delegation (like DelegationVisualizer) should
81+
override this method to create appropriate sub-visualizers.
82+
83+
Args:
84+
agent_id: The identifier of the sub-agent being spawned
85+
86+
Returns:
87+
A visualizer instance for the sub-agent, or None if sub-agent
88+
visualization is not supported
89+
"""
90+
return None

openhands-tools/openhands/tools/delegate/impl.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from openhands.sdk.tool.tool import ToolExecutor
1010
from openhands.tools.delegate.definition import DelegateObservation
1111
from openhands.tools.delegate.registration import get_agent_factory
12-
from openhands.tools.delegate.visualizer import DelegationVisualizer
1312

1413

1514
if TYPE_CHECKING:
@@ -130,14 +129,12 @@ def _spawn_agents(self, action: "DelegateAction") -> DelegateObservation:
130129
factory = get_agent_factory(agent_type)
131130
worker_agent = factory.factory_func(sub_agent_llm)
132131

133-
if isinstance(parent_visualizer, DelegationVisualizer):
134-
sub_visualizer = DelegationVisualizer(
135-
name=agent_id,
136-
highlight_regex=parent_visualizer._highlight_patterns,
137-
skip_user_messages=parent_visualizer._skip_user_messages,
138-
)
139-
else:
140-
sub_visualizer = None
132+
# Use parent visualizer's create_sub_visualizer method if available
133+
# This allows custom visualizers (e.g., TUI-based) to create
134+
# appropriate sub-visualizers for their environment
135+
sub_visualizer = None
136+
if parent_visualizer is not None:
137+
sub_visualizer = parent_visualizer.create_sub_visualizer(agent_id)
141138

142139
sub_conversation = LocalConversation(
143140
agent=worker_agent,
@@ -211,13 +208,13 @@ def _delegate_tasks(self, action: "DelegateAction") -> "DelegateObservation":
211208
results = {}
212209
errors = {}
213210

214-
# Get the parent agent's name from the visualizer
211+
# Get the parent agent's name from the visualizer if available
215212
parent_conversation = self.parent_conversation
216213
parent_name = None
217214
if hasattr(parent_conversation, "_visualizer"):
218215
visualizer = parent_conversation._visualizer
219-
if isinstance(visualizer, DelegationVisualizer):
220-
parent_name = visualizer._name
216+
if visualizer is not None:
217+
parent_name = getattr(visualizer, "_name", None)
221218

222219
def run_task(
223220
agent_id: str,

openhands-tools/openhands/tools/delegate/visualizer.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,24 @@ def __init__(
6161
)
6262
self._name = name
6363

64+
def create_sub_visualizer(self, agent_id: str) -> "DelegationVisualizer":
65+
"""Create a visualizer for a sub-agent during delegation.
66+
67+
Creates a new DelegationVisualizer instance for the sub-agent with
68+
the same configuration as the parent visualizer.
69+
70+
Args:
71+
agent_id: The identifier of the sub-agent being spawned
72+
73+
Returns:
74+
A new DelegationVisualizer configured for the sub-agent
75+
"""
76+
return DelegationVisualizer(
77+
name=agent_id,
78+
highlight_regex=self._highlight_patterns,
79+
skip_user_messages=self._skip_user_messages,
80+
)
81+
6482
@staticmethod
6583
def _format_agent_name(name: str) -> str:
6684
"""

tests/sdk/conversation/test_visualizer.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,3 +504,15 @@ def test_visualizer_conversation_state_update_event_skipped():
504504
block = visualizer._create_event_block(event)
505505
# Should return None to skip visualization
506506
assert block is None
507+
508+
509+
def test_default_visualizer_create_sub_visualizer_returns_none():
510+
"""Test that DefaultConversationVisualizer.create_sub_visualizer returns None.
511+
512+
This is the expected default behavior - base visualizers don't support
513+
sub-agent visualization. Subclasses like DelegationVisualizer can override
514+
this to provide sub-agent visualizers.
515+
"""
516+
visualizer = DefaultConversationVisualizer()
517+
result = visualizer.create_sub_visualizer("test_agent")
518+
assert result is None

tests/tools/delegate/test_visualizer.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,36 @@ def test_delegation_visualizer_observation_event():
211211
assert block is not None
212212
# The block contains the Rule as the first element with the title
213213
assert "Main Delegator Agent Observation" in str(block.renderables[0])
214+
215+
216+
def test_delegation_visualizer_create_sub_visualizer():
217+
"""Test create_sub_visualizer creates a new visualizer for sub-agents."""
218+
parent_visualizer = DelegationVisualizer(
219+
name="main_delegator",
220+
highlight_regex={"test": "bold"},
221+
skip_user_messages=True,
222+
)
223+
224+
# Create sub-visualizer for a sub-agent
225+
sub_visualizer = parent_visualizer.create_sub_visualizer("lodging_expert")
226+
227+
# Verify sub-visualizer is a DelegationVisualizer
228+
assert isinstance(sub_visualizer, DelegationVisualizer)
229+
# Verify sub-visualizer has the correct agent name
230+
assert sub_visualizer._name == "lodging_expert"
231+
# Verify settings are inherited from parent
232+
assert sub_visualizer._highlight_patterns == {"test": "bold"}
233+
assert sub_visualizer._skip_user_messages is True
234+
235+
236+
def test_delegation_visualizer_create_sub_visualizer_with_defaults():
237+
"""Test create_sub_visualizer works with default parent settings."""
238+
parent_visualizer = DelegationVisualizer(name="parent")
239+
240+
sub_visualizer = parent_visualizer.create_sub_visualizer("child_agent")
241+
242+
assert isinstance(sub_visualizer, DelegationVisualizer)
243+
assert sub_visualizer._name == "child_agent"
244+
# Default values should be inherited
245+
assert sub_visualizer._highlight_patterns is not None # Has default patterns
246+
assert sub_visualizer._skip_user_messages is False

0 commit comments

Comments
 (0)