Skip to content

feat: add rerun_actions method to replay conversation actions - #2351

Merged
csmith49 merged 7 commits into
mainfrom
feature/conversation-rerun-actions
Mar 9, 2026
Merged

csmith49 merged 7 commits into
mainfrom
feature/conversation-rerun-actions

Conversation

@csmith49

@csmith49 csmith49 commented Mar 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Add ability to re-execute all actions from a conversation's event history. This is useful for reproducing environment state from a saved conversation without re-running the LLM.

Changes

  • Add rerun_actions() method to LocalConversation that iterates through ActionEvents and re-executes them
  • Returns bool indicating success/failure (early exit on first failure)
  • Optional rerun_log_path parameter to save events using EventLog (memory-efficient incremental writes)
  • Add comprehensive test suite with real file I/O operations (10 tests)

Usage

# Basic usage - returns True if all actions succeed
conversation = Conversation(agent=agent, workspace=".")
success = conversation.rerun_actions()

# With logging - saves ActionEvent + ObservationEvent pairs to disk
success = conversation.rerun_actions(rerun_log_path="./rerun_events")

API

def rerun_actions(self, rerun_log_path: str | Path | None = None) -> bool:
    """Re-execute all actions from the conversation's event history.
    
    Args:
        rerun_log_path: Optional directory path to save a rerun event log.
            If provided, events will be written incrementally to disk using
            EventLog, avoiding memory buildup for large conversations.
    
    Returns:
        True if all actions executed successfully, False if any failed
    
    Raises:
        KeyError: If a tool from the original conversation is not available
    """

EventLog Format

When rerun_log_path is provided, events are stored using EventLog:

rerun_log_path/
└── events/
    ├── event-0-<action1_id>.json   # Original ActionEvent
    ├── event-1-<obs1_id>.json      # New ObservationEvent
    ├── event-2-<action2_id>.json   # Original ActionEvent
    └── event-3-<obs2_id>.json      # New ObservationEvent

Benefits:

  • Memory efficient: Events written immediately to disk, no list accumulation
  • Crash safe: Partial progress saved even if process crashes
  • Reusable format: Same EventLog API used for main conversation events

Implementation Details

  • Iterates through all events, finds ActionEvents with non-null actions
  • Resolves tools from agent's tools_map and calls executor
  • Early exit on failure: Stops immediately when a tool raises an exception
  • KeyError for missing tools: Configuration error (different from execution failure)
  • Does not modify event log: Original observations preserved
  • Tool error observations count as success: is_error=True means tool executed correctly

⚠️ Important Warning

Many tool operations are NOT idempotent:

  • File operations may fail if files already exist or were deleted
  • Terminal commands may have different effects on changed state
  • API calls may have side effects or return different results

The workspace should typically be reset before rerunning. This is the user's responsibility.

Testing

  • 10 tests covering:
    • Basic functionality with execution counters
    • Real file I/O operations (create, write, verify)
    • Non-idempotency scenarios (file exists error)
    • Early exit behavior (subsequent actions not executed)
    • EventLog verification
    • Missing tool handling
  • Pre-commit hooks pass (ruff format, lint, pyright, pycodestyle)

Agent Server images for this PR

• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22 Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:3fa3ce6-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-3fa3ce6-python \
  ghcr.io/openhands/agent-server:3fa3ce6-python

All tags pushed for this build

ghcr.io/openhands/agent-server:3fa3ce6-golang-amd64
ghcr.io/openhands/agent-server:3fa3ce6-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:3fa3ce6-golang-arm64
ghcr.io/openhands/agent-server:3fa3ce6-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:3fa3ce6-java-amd64
ghcr.io/openhands/agent-server:3fa3ce6-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:3fa3ce6-java-arm64
ghcr.io/openhands/agent-server:3fa3ce6-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:3fa3ce6-python-amd64
ghcr.io/openhands/agent-server:3fa3ce6-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-amd64
ghcr.io/openhands/agent-server:3fa3ce6-python-arm64
ghcr.io/openhands/agent-server:3fa3ce6-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-arm64
ghcr.io/openhands/agent-server:3fa3ce6-golang
ghcr.io/openhands/agent-server:3fa3ce6-java
ghcr.io/openhands/agent-server:3fa3ce6-python

About Multi-Architecture Support

  • Each variant tag (e.g., 3fa3ce6-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 3fa3ce6-python-amd64) are also available if needed

Add ability to re-execute all actions from a conversation's event history.
This is useful for reproducing environment state from a saved conversation.

Changes:
- Add rerun_actions() method to LocalConversation that iterates through
  ActionEvents and re-executes them using their original action parameters
- Add rerun parameter to LocalConversation.__init__ to trigger rerun
  during initialization
- Add rerun parameter to Conversation factory class
- Add validation that rerun is not supported for RemoteConversation

The original observations in the event log are preserved; new observations
from the rerun are returned but not persisted.

WARNING: Many tool operations are not idempotent (file creation, API calls,
etc). This feature is intended for advanced use cases where the user
understands results may differ from the original run.

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

github-actions Bot commented Mar 6, 2026 •

Copy link
Copy Markdown
Contributor

API breakage checks (Griffe)

Result: Passed

Action log

@github-actions

github-actions Bot commented Mar 6, 2026 •

Copy link
Copy Markdown
Contributor

Agent server REST API breakage checks (OpenAPI)

Result: Failed

Log excerpt (first 1000 characters)
{"asctime": "2026-03-09 15:01:10,735", "levelname": "WARNING", "name": "openhands.agent_server.config", "filename": "config.py", "lineno": 173, "message": "\u26a0\ufe0f OH_SECRET_KEY was not defined. Secrets will not be persisted between restarts."}
::error title=openhands-agent-server REST API::Breaking REST API change detected without MINOR version bump (1.12.0 -> 1.12.0).

Breaking REST API changes detected compared to baseline release:
- the 'file' request property type/format changed from 'string'/'' to 'string'/'binary'
/home/runner/work/software-agent-sdk/software-agent-sdk/.venv/lib/python3.13/site-packages/litellm/llms/custom_httpx/async_client_cleanup.py:66: DeprecationWarning: There is no current event loop
  loop = asyncio.get_event_loop()

Action log

@github-actions

github-actions Bot commented Mar 6, 2026 •

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/conversation/impl
   local_conversation.py3932394%288, 293, 321, 364, 382, 398, 460, 634–635, 638, 790, 798, 800, 811, 813–815, 840, 1033, 1037, 1107, 1114–1115
TOTAL19801576370% 

@csmith49

csmith49 commented Mar 6, 2026

Copy link
Copy Markdown
Contributor Author

@OpenHands I think we only want to expose the rerun_actions method, we don't need the init flag.

@openhands-ai

openhands-ai Bot commented Mar 6, 2026

Copy link
Copy Markdown

I'm on it! csmith49 can track my progress at all-hands.dev

Remove the rerun parameter from Conversation factory and LocalConversation
constructors. Users should call rerun_actions() method directly instead.

Changes:
- Remove rerun parameter from Conversation.__new__ overloads
- Remove rerun parameter from LocalConversation.__init__
- Remove automatic rerun execution in LocalConversation constructor
- Remove validation for rerun with RemoteConversation
- Remove related tests for init flag functionality

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai

openhands-ai Bot commented Mar 6, 2026

Copy link
Copy Markdown

Summary

I've addressed @csmith49's review comment by removing the rerun init flag from the constructors while keeping the rerun_actions() method available for direct use.

Changes Made

Checklist:

  • ✅ Removed rerun: bool = False parameter from Conversation.__new__ (both @overload signatures and main implementation)
  • ✅ Removed rerun: bool = False parameter from LocalConversation.__init__
  • ✅ Removed self._rerun_requested attribute and automatic rerun execution in constructor
  • ✅ Removed validation check for rerun with RemoteConversation
  • ✅ Removed 2 tests related to init flag (test_rerun_actions_called_during_init, test_rerun_flag_accepted_by_conversation_factory)
  • ✅ Kept rerun_actions() method intact for manual use
  • ✅ All pre-commit checks pass
  • ✅ All 6 remaining tests pass
  • ✅ Changes pushed to feature/conversation-rerun-actions

Net change: 3 files modified, 1 insertion, 55 deletions

New usage pattern:

conversation = Conversation(agent=agent, workspace=".")
observations = conversation.rerun_actions()  # Call method directly

@csmith49
csmith49 marked this pull request as ready for review March 6, 2026 17:13

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Needs improvement - The core design needs reconsideration. The stated goal is "reproducing environment state" but observations aren't persisted to the conversation, defeating this purpose. Additionally, the rerun parameter mentioned in the PR description isn't in the diff, suggesting incomplete implementation.

Comment thread openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py Outdated
Comment thread openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py
Comment thread openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py
Comment thread openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py
Comment thread tests/sdk/conversation/local/test_rerun_actions.py
Comment thread tests/sdk/conversation/local/test_rerun_actions.py Outdated
…run_actions

Address PR review comments on testing:

1. Test isolation: Replace manual registry snapshot/restore with monkeypatch
   fixture for cleaner per-test isolation that automatically cleans up

2. Real file operations: Add new test tools (FileWriteTool, FileCreateTool,
   FailingTool) that perform actual file I/O operations

3. State verification: test_rerun_reproduces_file_state verifies that files
   created during rerun are actually persisted to disk, then verifies
   recreation after deletion

4. Non-idempotency: test_rerun_non_idempotent_failure demonstrates the
   documented warning about non-idempotent operations (file creation fails
   if file already exists)

5. Partial failure: test_rerun_partial_failure verifies rerun continues
   processing after failures and returns only successful observations

6. Multiple files: test_rerun_multiple_files verifies sequential file
   operations including subdirectory creation

Co-authored-by: openhands <openhands@all-hands.dev>
…tional log

Simplify the rerun_actions API based on PR feedback:

1. Return boolean: True if all actions succeed, False if any raises exception
   - Tool returning error observation (is_error=True) still counts as success
   - Only exceptions cause failure return

2. Add rerun_log_path parameter: Optional path to save JSONL event log
   - Each entry contains action_index, tool_name, action, observation
   - Partial log saved on failure for debugging

3. Early exit on failure: Stop immediately when a tool raises an exception
   - Prevents cascading failures from corrupted state
   - Partial log available for diagnosing what succeeded

4. Keep KeyError for missing tools: Configuration error (can't proceed)
   - Different from execution failure (returns False)

5. No workspace reset: User responsibility to ensure clean state
   - Too many execution modes to handle automatically

Updated tests to verify:
- Boolean return values
- Rerun log contents via JSONL parsing
- Early exit behavior (subsequent actions not executed)
- Partial log saved on failure

Co-authored-by: openhands <openhands@all-hands.dev>
@csmith49
csmith49 requested a review from all-hands-bot March 6, 2026 17:50

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Acceptable - Core logic is sound, tests are excellent. Minor improvements suggested below.

Comment thread openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py Outdated
Comment thread openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py Outdated
Comment thread tests/sdk/conversation/local/test_rerun_actions.py
Address PR review feedback:

1. Move imports to top of file (no inline imports):
   - Add EventLog, LocalFileStore, ObservationEvent imports at module level

2. Use EventLog for memory-efficient logging:
   - Replace in-memory list with EventLog that writes incrementally to disk
   - Avoids memory buildup for conversations with thousands of actions
   - Events are written immediately, even if process crashes

3. Store actual Event objects:
   - Append original ActionEvent from conversation
   - Create and append ObservationEvent with new observation
   - Log structure: events/ directory with individual event JSON files

4. Update tests to use EventLog for verification:
   - Read back events using EventLog API
   - Verify ObservationEvent.observation properties directly

Co-authored-by: openhands <openhands@all-hands.dev>
@csmith49
csmith49 requested a review from all-hands-bot March 6, 2026 18:19

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Good taste - Clean implementation with excellent test coverage. All previous review concerns addressed (memory efficiency via EventLog, early exit error handling, real I/O tests). Ready to merge.

@enyst

enyst commented Mar 8, 2026 •

Copy link
Copy Markdown
Member

Interesting, thank you for this. This sounds like the old replay that hadn't made it into V1 yet?

@OpenHands Do a /codereview-roasted on this PR. Fully understand the goal and investigate implementation and code design.

Publish your review using gh api (not a comment, you are allowed to review)

Then take a step back, and look at your review section by section with fresh eyes for a reassessment: is it as minor as it seems, is it as major as it seemed, do suggestions respect the design patterns in the codebase or does the PR code design does; respond to the previous review with your reassessment.

@openhands-ai

openhands-ai Bot commented Mar 8, 2026

Copy link
Copy Markdown

I'm on it! enyst can track my progress at all-hands.dev

@enyst enyst left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taste Rating: 🟡 Acceptable

[CRITICAL ISSUES]

  • local_conversation.py lines 1005-1010 Correctness: Actions tied to tools without executors are silently skipped, yet rerun_actions still returns True. That violates the contract of “re-execute all actions” and can mask incomplete reruns. Treat this as a configuration error like execute_tool (raise NotImplementedError or return False) or explicitly fail when an action is skipped.

[IMPROVEMENT OPPORTUNITIES]

  • local_conversation.py lines 976-1029 Logging robustness: Reusing rerun_log_path can fail because EventLog.append rejects duplicate event IDs, and the failure is reported as a tool-execution failure. Enforce an empty log directory or create a per-run subdir/new IDs to prevent false negatives.

[TESTING GAPS]

  • None.

VERDICT:
❌ Needs rework: fix the silent skip so “success” means all actions actually ran.

KEY INSIGHT:
If rerun claims success while skipping actions, it’s lying to the caller.

@openhands-ai

This comment was marked as duplicate.

@enyst

enyst commented Mar 8, 2026

Copy link
Copy Markdown
Member

Yes — it’s conceptually similar to the old “replay” idea from OpenHands#6049, but narrower: this PR re-executes only ActionEvents to recreate environment state without re-running the LLM or rebuilding full conversation flow.

✅ Review published via GitHub API (COMMENT):
#2351 (review)


Reassessment (fresh eyes)

Critical issue (skip tools without executor)

Still stands as significant. Returning True while silently skipping actions contradicts the method contract and diverges from execute_tool, which raises on missing executors. This is a correctness issue, not just a preference.

Logging robustness (rerun_log_path reuse)

This is more of a minor usability pitfall. It won’t affect most runs if users use a fresh log dir, but the current behavior can surface as a confusing “tool failure” when it’s really a duplicate-ID write issue. Worth addressing, but not a blocker if documented/guarded.

Design alignment

Overall design is consistent with existing patterns (EventLog reuse, explicit agent init, no hidden state mutations). The main mismatch is the silent skip behavior, which conflicts with the codebase’s preference for explicit failures over silent no-ops.

If you want, I can help draft a follow-up patch to address the executor skip (and optionally the log reuse behavior).

@enyst enyst left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you!

@csmith49
csmith49 merged commit 703ce44 into main Mar 9, 2026
21 checks passed
@csmith49
csmith49 deleted the feature/conversation-rerun-actions branch March 9, 2026 15:04
StressTestor pushed a commit to StressTestor/software-agent-sdk that referenced this pull request Jun 1, 2026
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.

4 participants