fix(workspace): preserve caller's AgentContext in load_skills_from_agent_server() - #4876
fix(workspace): preserve caller's AgentContext in load_skills_from_agent_server()#4876vnktadithya wants to merge 6 commits into
Conversation
|
📁 PR Artifacts Notice This PR contains a |
There was a problem hiding this comment.
🟡 Changes recommended
A couple of small convention/maintainability issues (change-narration comments and brittle default assertions) should be cleaned up before merging.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR fixes a workspace-context preservation bug by ensuring RemoteWorkspace.load_skills_from_agent_server() can update only skills / load_public_skills while preserving a caller-provided AgentContext (including current_datetime), and mirrors the signature/forwarding in OpenHandsCloudWorkspace to keep subclass behavior consistent.
Changes:
- Add optional
base_context: AgentContext | NonetoRemoteWorkspace.load_skills_from_agent_server()and returnbase_context.model_copy(update=...)instead of constructing a freshAgentContext. - Mirror the
base_contextparameter and forwarding inOpenHandsCloudWorkspace.load_skills_from_agent_server(). - Add regression tests covering preservation behavior (including fallback and
current_datetime) and subclass forwarding; include a.pr/verification script.
File summaries
| File | Description |
|---|---|
openhands-sdk/openhands/sdk/workspace/remote/base.py |
Adds base_context and preserves caller context via model_copy(update=...) while updating loaded skills + load_public_skills. |
openhands-workspace/openhands/workspace/cloud/workspace.py |
Mirrors signature and forwards base_context through the override to the base implementation. |
tests/sdk/workspace/remote/test_remote_workspace.py |
Adds unit tests validating base-context preservation, fallback behavior, legacy behavior when omitted, and current_datetime preservation. |
tests/workspace/test_cloud_workspace_repos.py |
Adds a regression test ensuring the cloud workspace override forwards base_context. |
.pr/verify_base_context_preserved.py |
Adds an end-to-end verification script demonstrating preservation on both workspace classes (PR artifact). |
Review details
Suppressed comments (1)
openhands-workspace/openhands/workspace/cloud/workspace.py:943
- Remove the inline “# new” comment on the forwarded argument; it’s change-narration noise and will drift over time.
base_context=base_context, # new
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
🚦 CI is currently failing on this PR's latest commit. Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request This is an automated check - no AI was used to generate this comment. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
- remove change-narration comments - compare test assertions against AgentContext() defaults consistently
|
|
HUMAN:
The base context, which is already present, is being overwritten by the
load_skills_from_agent_serverfunction. This function's job is to explicitly changeskillsandload_public_skills, and to preserve all the existing context. But it was creating a newAgentContextobject and overwriting all the existing parameters.AGENT:
Why
RemoteWorkspace.load_skills_from_agent_server()always constructed a brand-newAgentContextfrom scratch when returning, discarding every field of anyAgentContextthe caller already held exceptskillsandload_public_skills. This silently resetload_user_skills,load_project_skills,disabled_skills,marketplace_path, both message suffixes, and — less obviously —current_datetime, which has adefault_factoryrather than a static default, so it got re-stamped to a fresh timestamp on every call rather than preserved.OpenHandsCloudWorkspace.load_skills_from_agent_server()inopenhands-workspace/openhands/workspace/cloud/workspace.pyre-declares this method's full signature and forwards every argument tosuper().load_skills_from_agent_server(...)— done deliberately sogriffe(this repo's API-compatibility checker) doesn't flag inherited-method removal from the subclass. That means the fix has to be mirrored there too, orOpenHandsCloudWorkspacecallers would silently never get access to the new capability while its docstring still claims parity withRemoteWorkspace.Originally flagged as an out-of-scope "optional companion change" in #4542 while fixing the
load_memorypropagation bug (#4566) — noted there as worth doing on its own merits but an independent code path from that fix.Summary
base_context: AgentContext | None = Noneparameter toRemoteWorkspace.load_skills_from_agent_server()(openhands-sdk/openhands/sdk/workspace/remote/base.py). When provided, the method now returnsbase.model_copy(update={"skills": ..., "load_public_skills": ...})instead of constructing a freshAgentContext(...), so every other field the caller configured survives. When omitted (default), behavior is unchanged from today.OpenHandsCloudWorkspaceoverride (openhands-workspace/openhands/workspace/cloud/workspace.py) so the fix isn't silently unavailable on that class.tests/sdk/workspace/remote/test_remote_workspace.py(base context preserved when skills are found, preserved on the empty-skills fallback path, no-base-context call matches legacy behavior,current_datetimespecifically survives a round-trip) and 1 new test intests/workspace/test_cloud_workspace_repos.pyconfirming theOpenHandsCloudWorkspaceoverride actually forwardsbase_contextthrough to the fix.Issue Number
Fixes #4863
How to Test
Unit tests:
Result: 71 passed (67 existing + 4 new, all unchanged existing tests still green since
base_contextdefaults toNoneand preserves today's behavior when omitted).Result: 65 passed (64 existing + 1 new).
Unit tests alone rely on mocked responses, so I also ran an end-to-end pass exercising real
AgentContextconstruction and real Pydanticmodel_copybehavior — the only thing mocked is the outbound HTTP call to the agent-server, since no live sandbox is available here:Output:
Acceptance criteria (from #4863)
load_skills_from_agent_server()accepts an optional baseAgentContextinstead of always constructing one from scratch — confirmed in the diff (newbase_contextparameter,openhands-sdk/openhands/sdk/workspace/remote/base.py)skillsandload_public_skills—test_load_skills_from_agent_server_preserves_base_context_when_foundcurrent_datetimefrom the base context is preserved, not regenerated —test_load_skills_from_agent_server_preserves_current_datetimetest_load_skills_from_agent_server_without_base_context_matches_legacy; all pre-existing tests intest_remote_workspace.pypass unchangedtest_load_skills_from_agent_server_preserves_base_context_when_found; (2) base context preserved on the empty-skills fallback path —test_load_skills_from_agent_server_preserves_base_context_on_fallback; (3) no-base-context call matches legacy behavior —test_load_skills_from_agent_server_without_base_context_matches_legacy; (4)current_datetimespecifically survives a round-trip —test_load_skills_from_agent_server_preserves_current_datetimeOpenHandsCloudWorkspace.load_skills_from_agent_server()accepts and forwardsbase_contexttosuper().load_skills_from_agent_server(...), with test coverage confirming preserved fields survive through the override —test_load_skills_from_agent_server_preserves_base_context(tests/workspace/test_cloud_workspace_repos.py)Video/Screenshots
No video — this is a backend-only change with no UI. The
.pr/verify_base_context_preserved.pyscript above is the functional evidence: it drives realRemoteWorkspaceandOpenHandsCloudWorkspaceinstances through realAgentContext/model_copycode, not pytest mocks of the fix itself.Type
Notes
User-facing:
load_skills_from_agent_server()now accepts an optionalbase_contextparameter on bothRemoteWorkspaceandOpenHandsCloudWorkspace. Omitting it (every existing call site today) leaves behavior unchanged — not a breaking change.Other
RemoteWorkspacesubclasses —ApptainerWorkspace,APIRemoteWorkspace,DockerWorkspace,DockerDevWorkspace— inherit the method directly without overriding it, so they receivebase_contextsupport automatically with no changes needed.AsyncRemoteWorkspacedoesn't implement this method at all and is unaffected. Confirmed via a repo-wide search for every call site and override ofload_skills_from_agent_serverbefore opening this PR.Checked this change against
.github/scripts/check_sdk_api_breakage.py(the griffe-based API-breakage gate this repo runs in CI): its scope, per its own docstring and its full test suite, is limited to detecting removed exports/members against the last published release. Adding an optional parameter with a default is purely additive and outside what that check inspects, so no MINOR version bump is required for this change alone.model_copy(update={...})performs a shallow copy, so list-valued fields (e.g.disabled_skills) on the returned context share the same underlying list object as the caller'sbase_contextunless a fresh one is supplied per call. This is the same shallow-copy convention already relied on by_with_load_memory()inconversation_service.pyfrom #4566, not a new risk introduced here.Related: #4542 (root issue), #4566 (merged fix for the
load_memorypropagation half of that issue — this is the other half, called out there as independent and deferred).