feat(workspace): Add completion callback support to RemoteWorkspace#3110
feat(workspace): Add completion callback support to RemoteWorkspace#3110
Conversation
Python API breakage checks — ✅ PASSEDResult: ✅ PASSED |
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
Coverage Report •
|
||||||||||||||||||||||||||||||
…ase class Move callback functionality to RemoteWorkspace base class so all subclasses (APIRemoteWorkspace, OpenHandsCloudWorkspace) can use it. On exit, the workspace can POST completion status to a configured callback URL. Environment variables (read via _init_callback_settings): - AUTOMATION_CALLBACK_URL: URL to POST completion status to - AUTOMATION_CALLBACK_API_KEY: Bearer token for callback auth (optional) - AUTOMATION_RUN_ID: Run ID to include in callback payload (optional) Base class changes: - Added _init_callback_settings() method for subclasses to call - Added _send_completion_callback() method for __exit__ to call - Updated register_conversation() to actually store the conversation_id - Updated conversation_id property to return stored value APIRemoteWorkspace changes: - Call _init_callback_settings() in model_post_init() - Call _send_completion_callback() in __exit__() Co-authored-by: openhands <openhands@all-hands.dev>
e6c660e to
756071e
Compare
Remove private attributes for automation callback settings and read environment variables directly in _send_completion_callback() instead. This keeps the callback logic self-contained and avoids leaking into other implementations that extend RemoteWorkspace. Co-authored-by: openhands <openhands@all-hands.dev>
Add __exit__ override to RemoteWorkspace base class that calls _send_completion_callback() so all subclasses get the behavior automatically. APIRemoteWorkspace now calls super().__exit__() instead of calling the callback directly. Co-authored-by: openhands <openhands@all-hands.dev>
all-hands-bot
left a comment
There was a problem hiding this comment.
🟢 Good taste - Clean implementation that follows existing patterns from OpenHandsCloudWorkspace.
Key strengths:
- Backward compatible (opt-in via env vars)
- Follows the same pattern as OpenHandsCloudWorkspace callback implementation
- Comprehensive test coverage
- Best-effort error handling prevents callback failures from breaking cleanup
- Properly stores conversation_id for inclusion in callback payload
RISK ASSESSMENT
🟢 LOW - Purely additive feature with no impact on existing behavior unless explicitly enabled via environment variables.
VERDICT: ✅ Worth merging - Extends callback functionality to all RemoteWorkspace implementations in a clean, testable way.
all-hands-bot
left a comment
There was a problem hiding this comment.
✅ QA Report: PASS
Completion callback functionality works end-to-end as specified — all environment variables, success/failure cases, and optional fields verified through actual HTTP requests.
Does this PR achieve its stated goal?
Yes. The PR successfully adds completion callback support to the RemoteWorkspace base class, enabling all remote workspace implementations to send completion status via HTTP POST on exit. I verified the implementation by:
- Starting an HTTP server to receive callbacks
- Creating
APIRemoteWorkspaceinstances with various callback configurations - Exercising exit paths (success and failure)
- Inspecting the actual HTTP requests sent
The callback correctly handles all documented environment variables (AUTOMATION_CALLBACK_URL, AUTOMATION_CALLBACK_API_KEY, AUTOMATION_RUN_ID), includes conversation IDs when registered, and gracefully no-ops when disabled.
| Phase | Result |
|---|---|
| Environment Setup | ✅ Dependencies installed, build succeeded |
| CI Status | ✅ All checks passing (sdk-tests, tools-tests, pre-commit, etc.) |
| Functional Verification | ✅ 5 scenarios verified with actual HTTP requests |
Functional Verification
Test 1: Successful Workspace Exit with Full Configuration
Step 1 — Set up callback receiver and environment:
Started HTTP server on localhost:8888 to receive callbacks.
Configured environment:
export AUTOMATION_CALLBACK_URL='http://localhost:8888/callback'
export AUTOMATION_CALLBACK_API_KEY='test-api-key-12345'
export AUTOMATION_RUN_ID='run-qa-test-001'Step 2 — Exercise workspace with successful exit:
Ran test script that creates APIRemoteWorkspace, registers conversation ID 'conv-qa-test-123', and exits normally:
workspace.register_conversation('conv-qa-test-123')
workspace.__enter__()
workspace.__exit__(None, None, None) # Successful exitStep 3 — Verify callback received:
Server logs showed:
{
"status": "COMPLETED",
"run_id": "run-qa-test-001",
"conversation_id": "conv-qa-test-123"
}SDK log confirmed:
Completion callback sent (COMPLETED): 200
This confirms the callback is sent on successful exit with all expected fields.
Test 2: Failed Workspace Exit with Error
Step 1 — Configure environment without API key:
export AUTOMATION_CALLBACK_URL='http://localhost:8888/callback'
export AUTOMATION_RUN_ID='run-qa-test-002'
# No AUTOMATION_CALLBACK_API_KEY setStep 2 — Exercise workspace with exception:
Simulated workspace failure:
exc = RuntimeError("Simulated workspace failure for QA testing")
workspace.__exit__(type(exc), exc, None)Step 3 — Verify FAILED callback:
Server received:
{
"status": "FAILED",
"run_id": "run-qa-test-002",
"error": "Simulated workspace failure for QA testing"
}This confirms:
- Status changes to "FAILED" on exception
- Error message is included in payload
- No conversation_id (none was registered)
- No Authorization header (API key not set)
Test 3: Callback Disabled (No URL)
Step 1 — Remove all callback environment variables:
unset AUTOMATION_CALLBACK_URL
unset AUTOMATION_CALLBACK_API_KEY
unset AUTOMATION_RUN_IDStep 2 — Exercise workspace:
workspace.__enter__()
workspace.__exit__(None, None, None)Step 3 — Verify no callback sent:
No "Completion callback sent" log message appeared. Server logs confirmed no new callback was received.
This confirms the callback is a no-op when AUTOMATION_CALLBACK_URL is not set.
Test 4: Authorization Header Verification
Step 1 — Set up with API key:
export AUTOMATION_CALLBACK_URL='http://localhost:8888/callback'
export AUTOMATION_CALLBACK_API_KEY='secret-key-abc123'Step 2 — Capture HTTP request details:
Used mock to intercept the actual httpx.Client.post() call and inspect headers.
Step 3 — Verify Authorization header:
Captured request:
Headers: {'Authorization': 'Bearer secret-key-abc123'}This confirms the Authorization header is correctly formatted as Bearer {api_key}.
Test 5: RemoteWorkspace Base Class Behavior
Step 1 — Test initial state:
workspace = RemoteWorkspace(host='http://localhost:8000', working_dir='/workspace')
assert workspace.conversation_id is None✅ conversation_id returns None initially
Step 2 — Register conversation:
workspace.register_conversation('conv-test-456')
assert workspace.conversation_id == 'conv-test-456'✅ conversation_id is stored and returned correctly
Step 3 — Verify persistence:
assert workspace.conversation_id == 'conv-test-456'✅ Value persists across property accesses
This confirms the base class changes (store conversation ID, return it via property) work as expected.
Issues Found
None.
Why
OpenHands Cloud workspace (
OpenHandsCloudWorkspace) supports sending completion callbacks on exit for automations viaAUTOMATION_CALLBACK_URL. This adds similar functionality to theRemoteWorkspacebase class so all remote workspace implementations can send completion callbacks when configured via environment variables.Summary
RemoteWorkspacebase class via_init_callback_settings()and_send_completion_callback()methodsregister_conversation()to store the conversation ID (was a no-op)APIRemoteWorkspaceto call the callback methods during init and exitIssue Number
N/A - User request
How to Test
Type
Notes
Environment variables:
AUTOMATION_CALLBACK_URL— URL to POST completion status toAUTOMATION_CALLBACK_API_KEY— Bearer token for callback auth (optional)AUTOMATION_RUN_ID— Run ID to include in callback payload (optional)This PR was created by an AI agent (OpenHands) on behalf of the user.
Agent Server images for this PR
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22-slimgolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:87af8d7-pythonRun
All tags pushed for this build
About Multi-Architecture Support
87af8d7-python) is a multi-arch manifest supporting both amd64 and arm6487af8d7-python-amd64) are also available if needed