Skip to content

[Bug]: RemoteWorkspace.load_skills_from_agent_server() discards the caller's AgentContext, resetting skill preferences beyond load_public_skills #4863

Description

@vnktadithya

Is there an existing issue for the same bug?

  • I have searched existing issues and this is not a duplicate.

Bug Description

RemoteWorkspace.load_skills_from_agent_server() in openhands-sdk/openhands/sdk/workspace/remote/base.py always constructs a brand-new AgentContext from scratch when it returns, instead of updating any AgentContext the caller already had. Every field other than skills and load_public_skills is silently reset to its class default — load_user_skills, load_project_skills, disabled_skills, marketplace_path, system_message_suffix, and user_message_suffix all disappear from the returned context regardless of what the caller had configured.

One instance of this is worth calling out specifically: current_datetime defaults to default_factory=lambda: datetime.now().astimezone(), not None. So a caller's AgentContext(current_datetime=<some fixed value>) doesn't just get dropped — it gets silently replaced with a freshly-generated "now" timestamp at whatever moment load_skills_from_agent_server() happened to run.

Confirmed still present on main at the two AgentContext(...) construction lines near the end of the function (currently ~lines 928–933):

if loaded_skills:
    agent_context = AgentContext(skills=loaded_skills, load_public_skills=False)
else:
    logger.warning("No skills loaded, falling back to public skills")
    agent_context = AgentContext(skills=[], load_public_skills=True)

Originally flagged as an out-of-scope "optional companion change" in #4542 while fixing the load_memory propagation bug (#4566) — noted there as worth doing on its own merits but an independent code path from that fix.

Note:

OpenHandsCloudWorkspace in openhands-workspace/openhands/workspace/cloud/workspace.py re-declares this method's full signature and forwards every argument to super().load_skills_from_agent_server(...) — done deliberately so griffe (this repo's API-compatibility checker) doesn't flag inherited-method removal from the subclass. That means any fix to the base method's signature must also be mirrored here, or OpenHandsCloudWorkspace callers silently never get access to the new capability while its docstring still claims parity with RemoteWorkspace.

Expected Behavior

load_skills_from_agent_server() should preserve any AgentContext fields the caller already set — marketplace_path, disabled_skills, message suffixes, current_datetime, etc. — and only change the two fields the function itself is responsible for: skills (the whole point of the call) and load_public_skills (set by the function's own found-vs-fallback logic). Accepting an optional base context and returning base_context.model_copy(update={"skills": ..., "load_public_skills": ...}) instead of constructing a fresh AgentContext(...) would make the helper non-destructive. AgentContext isn't a frozen model, so nothing forces this mechanically — it's the same convention the SDK already uses elsewhere for non-mutating updates on value-like objects, applied here for the same reason: avoid the caller having to know their prior configuration was reconstructed rather than preserved.

Actual Behavior

The reset is reproducible without a running agent-server by mocking the one network call (_call_skills_api) the function makes internally. Save this as repro_load_skills_context.py:

import time
from unittest.mock import patch

from openhands.sdk import RemoteWorkspace

workspace = RemoteWorkspace(
    host="https://agent-server.example.com", working_dir="/workspace"
)

with patch.object(
    workspace,
    "_call_skills_api",
    return_value=[{"name": "demo-skill", "content": "demo"}],
):
    _, context_1 = workspace.load_skills_from_agent_server()
    time.sleep(1.5)
    _, context_2 = workspace.load_skills_from_agent_server()

print("marketplace_path:", context_1.marketplace_path)
print("disabled_skills:", context_1.disabled_skills)
print("current_datetime (call 1):", context_1.current_datetime)
print("current_datetime (call 2):", context_2.current_datetime)
print("Same timestamp across calls?", context_1.current_datetime == context_2.current_datetime)

Run with:

uv run python repro_load_skills_context.py

Observed output:

[09/06/26 09:50:59] INFO     Loading skills via agent-server...                                                       base.py:907
[09/06/26 09:50:59] INFO     Loaded 1 skills                                                                          base.py:924
[09/06/26 09:51:01] INFO     Loading skills via agent-server...                                                       base.py:907
[09/06/26 09:51:01] INFO     Loaded 1 skills                                                                          base.py:924
marketplace_path: marketplaces/default.json
disabled_skills: []
current_datetime (call 1): 2026-09-06 09:50:59.622681+05:30
current_datetime (call 2): 2026-09-06 09:51:01.129209+05:30
Same timestamp across calls? False

There is no parameter on load_skills_from_agent_server() today that can change marketplace_path or disabled_skills away from their class defaults — the function offers no way to preserve them. And current_datetime differing between two calls 1.5 seconds apart, with no caller input at all, is direct proof it is being freshly generated on every call rather than sourced from any prior context.

Acceptance Criteria

  • load_skills_from_agent_server() accepts an optional base AgentContext instead of always constructing one from scratch
  • When a base context is provided, all of its fields survive the call except skills and load_public_skills (which the function's found/fallback logic continues to set as it does today)
  • current_datetime from the base context is preserved, not regenerated
  • Calling the function without a base context (every existing call site today) produces output equivalent to current behavior — no breaking change
  • Test coverage for: (1) base context preserved when skills are found, (2) base context preserved on the empty-skills fallback path, (3) no-base-context call matches legacy behavior, (4) current_datetime specifically survives a round-trip
  • OpenHandsCloudWorkspace.load_skills_from_agent_server() (openhands-workspace/openhands/workspace/cloud/workspace.py) accepts and forwards base_context to super().load_skills_from_agent_server(...), with test coverage confirming preserved fields survive through the override

Screenshots and Additional Context

Related: #4542 (root issue), #4566 (merged fix for the load_memory propagation half of that issue — this is the other half, called out there as independent and deferred).

I'd like to work on this — I'll open a PR shortly with regression tests covering the acceptance criteria above.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority:mediumFor bugs, a serious source of annoyance, but not blocking a large number of users.ready-for-devIssue meets development readiness criteriasdkskillsworkspace

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions