Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import os
import shutil
import json
from openhands.sdk import LLM, Conversation, Agent, Tool
from openhands.sdk.agent.replay_agent import SnapshotReplayAgent
from openhands.tools.terminal import TerminalTool

from dotenv import load_dotenv
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.

🟠 Important — python-dotenv isn't a project dependency. The example will ModuleNotFoundError for anyone running it from a clean checkout. The other examples/01_standalone_sdk/*.py scripts read env vars directly — please follow that pattern:

Suggested change
from dotenv import load_dotenv

(i.e. just delete lines 8 and 10 — the os.getenv(...) calls below already do the right thing if env vars are set externally).


load_dotenv()

#import logging module, then set logging level to DEBUG to see detailed logs during replay
import logging
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
Comment on lines +12 to +17
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.

🟡 Suggestion — module-level logging.basicConfig(level=DEBUG) is loud. Setting the root logger to DEBUG at import time floods the console with every dependency's debug output (litellm, httpx, etc.) and makes the example output hard to read. Consider removing it, or guarding behind something like if os.getenv("DEBUG"). The SDK already wires up its own logger via get_logger.


# Get path to this script's directory to ensure relative paths work correctly. convert it to string

script_dir = str(os.path.dirname(os.path.abspath(__file__)))



# --- SETUP ---
persistence_path = f"{script_dir}/hello_world_recorded"
drift_log = f"{script_dir}/replay_drift.jsonl"

# Cleanup from previous runs
for path in [persistence_path, drift_log]:
if os.path.exists(path):
if os.path.isdir(path):
shutil.rmtree(path)
else:
os.remove(path)

# 1. Setup LLM
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", None),
)

# --- PHASE 1: RECORDING ---
print("--- Phase 1: Recording Session ---")
shapshot_agent = Agent(
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.

🟡 Suggestion — typo:

Suggested change
shapshot_agent = Agent(
snapshot_agent = Agent(

(also update the agent=shapshot_agent reference on line 52)

llm=llm,
tools=[Tool(name=TerminalTool.name)],
)

conversation = Conversation(
agent=shapshot_agent,
workspace=os.getcwd(),
persistence_dir=persistence_path
)

conversation.send_message("Write 3 facts about the current project into FACTS.txt.")

conversation.run()
print(f"Recording complete. Files created in {persistence_path}/\n")


# --- PHASE 2: REPLAYING ---
print(f"--- Phase 2: Replaying Session from {persistence_path} ---")

# Initialize the replay agent ONLY AFTER the recording is finished.
# This ensures model_post_init can successfully discover the recorded events.
replay_agent = SnapshotReplayAgent(
llm=llm,
tools=[Tool(name=TerminalTool.name)],
replay_mode=True,
replay_persistence=persistence_path,
drift_log_path=drift_log,
)

replay_conversation = Conversation(
agent=replay_agent,
workspace=os.getcwd(),
persistence_dir=persistence_path
)
replay_conversation.run()

print("\n--- Replay Finished ---")

# --- VALIDATION ---
if os.path.exists(drift_log):
print(f"\nDrift log results ({drift_log}):")
with open(drift_log, 'r') as f:
for line in f:
data = json.loads(line)
print(f"Action: {data['Action']['tool_name']} -> {data['Action']['action'].get('command', data['Action']['action'].get('message'))}")
print(f"Drift: {data['Drift from expected observation']}")
else:
print("\nError: Drift log was not created.")

print("\nValidation complete!")
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.

🟡 Suggestion — example isn't picked up by the example-runner CI. tests/examples/test_examples.py::_TARGET_DIRECTORIES only globs *.py directly in the listed dirs, plus a few hard-coded subdir+main.py pairs. This file lives at 49_snapshot_replay_agent/hello_world_snapshot_replay.py, so it'll never run in CI and is likely to bit-rot. To wire it in:

  1. Rename the file to main.py (matching 37_llm_profile_store/main.py, 43_mixed_marketplace_skills/main.py).
  2. Add EXAMPLES_ROOT / "01_standalone_sdk" / "49_snapshot_replay_agent" to _TARGET_DIRECTORIES in tests/examples/test_examples.py.
  3. Print EXAMPLE_COST: {cost} at the end of the script (see 02_custom_tools.py:229 for the standard pattern), which the runner asserts on.

Otherwise this example is documentation-by-trust-me. 🙂

Loading