-
Notifications
You must be signed in to change notification settings - Fork 154
Feat: session recording agent's browser sessions #1731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
73 commits
Select commit
Hold shift + click to select a range
0cf7696
feat: add script injection support for browser session recording
openhands-agent 816e51b
feat: add browser_start_recording and browser_stop_recording tools
openhands-agent 11aedc3
test: add unit and E2E tests for browser session recording
openhands-agent 68c6b80
docs: add browser session recording example
openhands-agent 2ed2f9b
fix: add retry mechanism to browser recording and improve stub
openhands-agent 8aec4f9
Update 34_browser_session_recording.py
malhotra5 cd118f7
fix: use unpkg CDN for rrweb to fix MIME type issue
openhands-agent 0a7d3ee
Update 34_browser_session_recording.py
malhotra5 0f84145
fix: persist browser recording across page navigations
openhands-agent c9370c3
feat: auto-save browser recordings to file, return concise summary
openhands-agent 87c36a0
Update 34_browser_session_recording.py
malhotra5 b89df77
docs: update browser recording example with persistence_dir
openhands-agent de8481a
Update 34_browser_session_recording.py
malhotra5 d227c50
Merge branch 'feat/browser-session-recording' of https://github.com/O…
malhotra5 49e360a
fix persistence path check
malhotra5 326251f
Update 33_browser_session_recording.py
malhotra5 167def2
Remove fallback stub from browser recording; report failures directly
openhands-agent 4e15af4
Improve recording event flushing: periodic saves to numbered files
openhands-agent bba1480
Refactor: Extract injected JavaScript to constants at top of file
openhands-agent 9852b34
Fix: Check for existing files before saving recording events
openhands-agent c115d52
Fix: session recording periodic flush and CancelledError handling
openhands-agent 5b5b44a
Fix: start periodic flush task when recording is already active
openhands-agent 5108e3c
Fix: only start recording when agent explicitly requests it
openhands-agent 73e8480
Add unit tests for recording flush behavior
openhands-agent 1d28599
Potential fix for pull request finding 'Empty except'
malhotra5 b1adb11
Merge branch 'main' into feat/browser-session-recording
malhotra5 bf09d97
rename file
malhotra5 4ffb097
Fix unreachable except clause in test_browser_executor_e2e.py
openhands-agent ac4a6e5
Trigger CI re-run for docs check
openhands-agent f792a81
Refactor: Encapsulate recording state in RecordingSession class
openhands-agent a71051d
Fix concurrency race condition in browser recording flush
openhands-agent 8838b52
Fix browser_stop_recording API documentation contract
openhands-agent 8888ecd
Merge branch 'main' into feat/browser-session-recording
malhotra5 f4b7cae
Refactor RecordingSession to use EventBuffer and RecordingState
openhands-agent 50278b6
Remove backward compatibility code and update tests to use new API
openhands-agent 924453c
Fix file count reporting with existing files in save_dir
openhands-agent e423730
Refactor polling anti-pattern to use event-driven Promise-based waiting
openhands-agent 66cc91d
Remove size-based flushing for recording events
openhands-agent da2f6cf
Refactor: Move JavaScript code to separate files for better maintaina…
openhands-agent 1f74b0b
Replace mock-only recording tests with real behavior tests
openhands-agent db82d62
Merge branch 'main' into feat/browser-session-recording
malhotra5 9fe1520
feat: separate recordings into timestamped subfolders
openhands-agent 8f67fd6
Fix decorator exception handling to be more specific
openhands-agent 23e6404
Fix readOnlyHint for browser_stop_recording tool
openhands-agent eee270d
Remove unused save_dir parameter from _stop_recording
openhands-agent e9cfa9d
Document CDN dependency risk in RecordingConfig
openhands-agent 0409976
Optimize file numbering with one-time directory scan
openhands-agent 14edf83
Document removal of size-based flushing in EventBuffer
openhands-agent f143e0c
Remove size-based flushing documentation from EventBuffer
openhands-agent 947a9c1
Simplify directory naming: base_save_dir -> output_dir, save_dir -> s…
openhands-agent ec23c36
Clarify lock documentation: rename to _event_buffer_lock and fix term…
openhands-agent f3119ad
Simplify RecordingState enum to boolean _is_recording
openhands-agent f1b081c
Simplify error handling and improve logging for recording
openhands-agent 5b9be07
Refactor: Extract EventStorage from RecordingSession
openhands-agent a9b85b8
Fix: Look for recording files in timestamped subdirectory
openhands-agent 6ca3d4a
Merge branch 'main' into feat/browser-session-recording
xingyaoww cdbc830
Use .agent_tmp for persistence_dir in browser session recording example
openhands-agent 243b8a7
Revert persistence_dir change, save recordings to .agent_tmp instead
openhands-agent d45e701
Merge branch 'main' into feat/browser-session-recording
xingyaoww 53dface
refactor(recording): extract helper methods from start() to reduce co…
openhands-agent 1bae49e
fix(recording): cleanup recording session when browser session closes
openhands-agent 8fa4ab9
refactor(recording): move recording output dir to global constant and…
openhands-agent 3e2bfbf
docs(recording): add error handling policy documentation and inline c…
openhands-agent 91a3b7e
fix(async_executor): remove atexit handler to fix cleanup ordering
xingyaoww 0b0c7e3
fix: skip recording test when browser initialization fails
openhands-agent 8e1a6e0
Merge branch 'main' into feat/browser-session-recording
malhotra5 70ca3b2
fix: update test to check correct recording output directory
openhands-agent f283e00
Merge branch 'main' into feat/browser-session-recording
xingyaoww fe6f705
Revert "fix(async_executor): remove atexit handler to fix cleanup ord…
openhands-agent 0fcb494
feat: track consecutive flush failures and warn user
openhands-agent 80ba4b5
Merge branch 'main' into feat/browser-session-recording
xingyaoww a25e8c5
Enforce approval when PR is deemed worth merging
openhands-agent 98626e6
Merge branch 'main' into feat/browser-session-recording
xingyaoww File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
178 changes: 178 additions & 0 deletions
178
examples/01_standalone_sdk/38_browser_session_recording.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| """Browser Session Recording Example | ||
|
|
||
| This example demonstrates how to use the browser session recording feature | ||
| to capture and save a recording of the agent's browser interactions using rrweb. | ||
|
|
||
| The recording can be replayed later using rrweb-player to visualize the agent's | ||
| browsing session. | ||
|
|
||
| The recording will be automatically saved to the persistence directory when | ||
| browser_stop_recording is called. You can replay it with: | ||
| - rrweb-player: https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-player | ||
| - Online viewer: https://www.rrweb.io/demo/ | ||
| """ | ||
|
|
||
| import json | ||
| import os | ||
|
|
||
| from pydantic import SecretStr | ||
|
|
||
| from openhands.sdk import ( | ||
| LLM, | ||
| Agent, | ||
| Conversation, | ||
| Event, | ||
| LLMConvertibleEvent, | ||
| get_logger, | ||
| ) | ||
| from openhands.sdk.tool import Tool | ||
| from openhands.tools.browser_use import BrowserToolSet | ||
| from openhands.tools.browser_use.definition import BROWSER_RECORDING_OUTPUT_DIR | ||
|
|
||
|
|
||
| logger = get_logger(__name__) | ||
|
|
||
| # Configure LLM | ||
| api_key = os.getenv("LLM_API_KEY") | ||
| assert api_key is not None, "LLM_API_KEY environment variable is not set." | ||
| model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") | ||
| base_url = os.getenv("LLM_BASE_URL") | ||
| llm = LLM( | ||
| usage_id="agent", | ||
| model=model, | ||
| base_url=base_url, | ||
| api_key=SecretStr(api_key), | ||
| ) | ||
|
|
||
| # Tools - including browser tools with recording capability | ||
| cwd = os.getcwd() | ||
| tools = [ | ||
| Tool(name=BrowserToolSet.name), | ||
| ] | ||
|
|
||
| # Agent | ||
| agent = Agent(llm=llm, tools=tools) | ||
|
|
||
| llm_messages = [] # collect raw LLM messages | ||
|
|
||
|
|
||
| def conversation_callback(event: Event): | ||
| if isinstance(event, LLMConvertibleEvent): | ||
| llm_messages.append(event.to_llm_message()) | ||
|
|
||
|
|
||
| # Create conversation with persistence_dir set to save browser recordings | ||
| conversation = Conversation( | ||
| agent=agent, | ||
| callbacks=[conversation_callback], | ||
| workspace=cwd, | ||
| persistence_dir="./.conversations", | ||
| ) | ||
|
|
||
| # The prompt instructs the agent to: | ||
| # 1. Start recording the browser session | ||
| # 2. Browse to a website and perform some actions | ||
| # 3. Stop recording (auto-saves to file) | ||
| PROMPT = """ | ||
| Please complete the following task to demonstrate browser session recording: | ||
|
|
||
| 1. First, use `browser_start_recording` to begin recording the browser session. | ||
|
|
||
| 2. Then navigate to https://docs.openhands.dev/ and: | ||
| - Get the page content | ||
| - Scroll down the page | ||
| - Get the browser state to see interactive elements | ||
|
|
||
| 3. Next, navigate to https://docs.openhands.dev/openhands/usage/cli/installation and: | ||
| - Get the page content | ||
| - Scroll down to see more content | ||
|
|
||
| 4. Finally, use `browser_stop_recording` to stop the recording. | ||
malhotra5 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Events are automatically saved. | ||
| """ | ||
|
|
||
| print("=" * 80) | ||
| print("Browser Session Recording Example") | ||
| print("=" * 80) | ||
| print("\nTask: Record an agent's browser session and save it for replay") | ||
| print("\nStarting conversation with agent...\n") | ||
|
|
||
| conversation.send_message(PROMPT) | ||
| conversation.run() | ||
|
|
||
| print("\n" + "=" * 80) | ||
| print("Conversation finished!") | ||
| print("=" * 80) | ||
|
|
||
| # Check if the recording files were created | ||
| # Recordings are saved in BROWSER_RECORDING_OUTPUT_DIR/recording-{timestamp}/ | ||
| if os.path.exists(BROWSER_RECORDING_OUTPUT_DIR): | ||
| # Find recording subdirectories (they start with "recording-") | ||
| recording_dirs = sorted( | ||
| [ | ||
| d | ||
| for d in os.listdir(BROWSER_RECORDING_OUTPUT_DIR) | ||
| if d.startswith("recording-") | ||
| and os.path.isdir(os.path.join(BROWSER_RECORDING_OUTPUT_DIR, d)) | ||
| ] | ||
| ) | ||
|
|
||
| if recording_dirs: | ||
| # Process the most recent recording directory | ||
| latest_recording = recording_dirs[-1] | ||
| recording_path = os.path.join(BROWSER_RECORDING_OUTPUT_DIR, latest_recording) | ||
| json_files = sorted( | ||
| [f for f in os.listdir(recording_path) if f.endswith(".json")] | ||
| ) | ||
|
|
||
| print(f"\n✓ Recording saved to: {recording_path}") | ||
| print(f"✓ Number of files: {len(json_files)}") | ||
|
|
||
| # Count total events across all files | ||
| total_events = 0 | ||
| all_event_types: dict[int | str, int] = {} | ||
| total_size = 0 | ||
|
|
||
| for json_file in json_files: | ||
| filepath = os.path.join(recording_path, json_file) | ||
| file_size = os.path.getsize(filepath) | ||
| total_size += file_size | ||
|
|
||
| with open(filepath) as f: | ||
| events = json.load(f) | ||
|
|
||
| # Events are stored as a list in each file | ||
| if isinstance(events, list): | ||
| total_events += len(events) | ||
| for event in events: | ||
| event_type = event.get("type", "unknown") | ||
| all_event_types[event_type] = all_event_types.get(event_type, 0) + 1 | ||
|
|
||
| print(f" - {json_file}: {len(events)} events, {file_size} bytes") | ||
|
|
||
| print(f"✓ Total events: {total_events}") | ||
| print(f"✓ Total size: {total_size} bytes") | ||
| if all_event_types: | ||
| print(f"✓ Event types: {all_event_types}") | ||
|
|
||
| print("\nTo replay this recording, you can use:") | ||
| print( | ||
| " - rrweb-player: " | ||
| "https://github.com/rrweb-io/rrweb/tree/master/packages/rrweb-player" | ||
| ) | ||
| else: | ||
| print(f"\n✗ No recording directories found in: {BROWSER_RECORDING_OUTPUT_DIR}") | ||
| print(" The agent may not have completed the recording task.") | ||
| else: | ||
| print(f"\n✗ Observations directory not found: {BROWSER_RECORDING_OUTPUT_DIR}") | ||
| print(" The agent may not have completed the recording task.") | ||
|
|
||
| print("\n" + "=" * 100) | ||
| print("Conversation finished.") | ||
| print(f"Total LLM messages: {len(llm_messages)}") | ||
| print("=" * 100) | ||
|
|
||
| # Report cost | ||
| cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost | ||
| print(f"Conversation ID: {conversation.id}") | ||
| print(f"EXAMPLE_COST: {cost}") | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.