Skip to content

Commit e68d1ee

Browse files
authored
Merge branch 'main' into fix/remote-conversation-hook-config
2 parents aa0129f + 703ce44 commit e68d1ee

File tree

2 files changed

+737
-0
lines changed

2 files changed

+737
-0
lines changed

openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from openhands.sdk.agent.base import AgentBase
77
from openhands.sdk.context.prompts.prompt import render_template
88
from openhands.sdk.conversation.base import BaseConversation
9+
from openhands.sdk.conversation.event_store import EventLog
910
from openhands.sdk.conversation.exceptions import ConversationRunError
1011
from openhands.sdk.conversation.secret_registry import SecretValue
1112
from openhands.sdk.conversation.state import (
@@ -25,13 +26,16 @@
2526
DefaultConversationVisualizer,
2627
)
2728
from openhands.sdk.event import (
29+
ActionEvent,
2830
CondensationRequest,
2931
MessageEvent,
32+
ObservationEvent,
3033
PauseEvent,
3134
UserRejectObservation,
3235
)
3336
from openhands.sdk.event.conversation_error import ConversationErrorEvent
3437
from openhands.sdk.hooks import HookConfig, HookEventProcessor, create_hook_callback
38+
from openhands.sdk.io import LocalFileStore
3539
from openhands.sdk.llm import LLM, Message, TextContent
3640
from openhands.sdk.llm.llm_profile_store import LLMProfileStore
3741
from openhands.sdk.llm.llm_registry import LLMRegistry
@@ -958,6 +962,111 @@ def condense(self) -> None:
958962

959963
logger.info("Condensation request processed")
960964

965+
def rerun_actions(
966+
self,
967+
rerun_log_path: str | Path | None = None,
968+
) -> bool:
969+
"""Re-execute all actions from the conversation's event history.
970+
971+
This method iterates through all ActionEvents in the conversation and
972+
re-executes them using their original action parameters. Execution
973+
stops immediately if any tool call fails.
974+
975+
WARNING: This is an advanced feature intended for specific use cases
976+
such as reproducing environment state from a saved conversation. Many
977+
tool operations are NOT idempotent:
978+
979+
- File operations may fail if files already exist or were deleted
980+
- Terminal commands may have different effects on changed state
981+
- API calls may have side effects or return different results
982+
- Browser state may differ from the original session
983+
984+
Use this method only when you understand that:
985+
1. Results may differ from the original conversation
986+
2. Some actions may fail due to changed environment state
987+
3. The workspace should typically be reset before rerunning
988+
989+
Args:
990+
rerun_log_path: Optional directory path to save a rerun event log.
991+
If provided, events will be written incrementally to disk using
992+
EventLog, avoiding memory buildup for large conversations.
993+
994+
Returns:
995+
True if all actions executed successfully, False if any action failed.
996+
997+
Raises:
998+
KeyError: If a tool from the original conversation is not available.
999+
This is a configuration error (different from execution failure).
1000+
"""
1001+
# Ensure agent is initialized (loads plugins and initializes tools)
1002+
self._ensure_agent_ready()
1003+
1004+
# Set up rerun log if path provided
1005+
rerun_log: EventLog | None = None
1006+
if rerun_log_path is not None:
1007+
log_dir = Path(rerun_log_path)
1008+
log_dir.mkdir(parents=True, exist_ok=True)
1009+
file_store = LocalFileStore(str(log_dir))
1010+
rerun_log = EventLog(file_store, dir_path="events")
1011+
1012+
action_count = 0
1013+
1014+
for event in self._state.events:
1015+
if not isinstance(event, ActionEvent):
1016+
continue
1017+
if event.action is None:
1018+
# Skip actions that failed validation during original run
1019+
continue
1020+
1021+
action_count += 1
1022+
tool_name = event.tool_name
1023+
1024+
# Get the tool from the agent's tools_map
1025+
tool = self.agent.tools_map.get(tool_name)
1026+
if tool is None:
1027+
available_tools = list(self.agent.tools_map.keys())
1028+
raise KeyError(
1029+
f"Tool '{tool_name}' not found during rerun. "
1030+
f"Available tools: {available_tools}. "
1031+
f"Ensure the agent is configured with the same tools as the "
1032+
f"original conversation."
1033+
)
1034+
1035+
if not tool.executor:
1036+
logger.warning(
1037+
f"Skipping action {action_count}: "
1038+
f"tool '{tool_name}' has no executor"
1039+
)
1040+
continue
1041+
1042+
# Execute the tool with the original action
1043+
try:
1044+
logger.info(f"Rerunning action {action_count}: {tool_name}")
1045+
observation = tool(event.action, self)
1046+
1047+
# Log the action and observation incrementally
1048+
if rerun_log is not None:
1049+
# Append action event (copy from original)
1050+
rerun_log.append(event)
1051+
# Append observation event
1052+
obs_event = ObservationEvent(
1053+
source="environment",
1054+
tool_name=tool_name,
1055+
tool_call_id=event.tool_call_id,
1056+
observation=observation,
1057+
action_id=event.id,
1058+
)
1059+
rerun_log.append(obs_event)
1060+
except Exception as e:
1061+
logger.error(
1062+
f"Action {action_count} ({tool_name}) failed during rerun: {e}"
1063+
)
1064+
# Log is already written incrementally, just return failure
1065+
return False
1066+
1067+
logger.info(f"Rerun complete: {action_count} actions processed successfully")
1068+
return True
1069+
9611070
def execute_tool(self, tool_name: str, action: Action) -> Observation:
9621071
"""Execute a tool directly without going through the agent loop.
9631072

0 commit comments

Comments
 (0)