Skip to content

Commit 71b9751

Browse files
fix: address PR review — deep-copy events/state, clarify agent param
- Deep-copy events via model_copy(deep=True) so source stays immutable - Deep-copy agent_state via copy.deepcopy for mutable values - RemoteConversation.fork() now raises NotImplementedError when agent is passed (server doesn't support agent replacement yet) Co-authored-by: openhands <openhands@all-hands.dev>
1 parent c0aae2a commit 71b9751

File tree

2 files changed

+24
-15
lines changed

2 files changed

+24
-15
lines changed

openhands-sdk/openhands/sdk/conversation/impl/local_conversation.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import atexit
2+
import copy
23
import uuid
34
from collections.abc import Mapping
45
from pathlib import Path
@@ -368,15 +369,18 @@ def fork(
368369
tags=tags,
369370
)
370371

371-
# Copy events from source → fork
372+
# Deep-copy events from source → fork so the source stays immutable.
372373
for event in self._state.events:
373-
fork_conv._state.events.append(event)
374+
fork_conv._state.events.append(event.model_copy(deep=True))
374375

375-
# Copy runtime state that accumulated during the source conversation
376+
# Copy runtime state that accumulated during the source conversation.
377+
# activated_knowledge_skills is list[str] – strings are immutable so a
378+
# shallow list copy is sufficient. agent_state can hold arbitrary
379+
# mutable values, so deep-copy it.
376380
fork_conv._state.activated_knowledge_skills = list(
377381
self._state.activated_knowledge_skills
378382
)
379-
fork_conv._state.agent_state = dict(self._state.agent_state)
383+
fork_conv._state.agent_state = copy.deepcopy(self._state.agent_state)
380384

381385
# Copy title via tags if provided
382386
if title is not None:

openhands-sdk/openhands/sdk/conversation/impl/remote_conversation.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,8 +1317,9 @@ def fork(
13171317
Args:
13181318
conversation_id: ID for the forked conversation (auto-generated
13191319
on the server if ``None``).
1320-
agent: Agent for the fork (serialised and sent to the server).
1321-
Defaults to a deep-copy of the source agent on the server.
1320+
agent: **Not supported for remote conversations.** Passing a
1321+
non-``None`` value raises ``NotImplementedError``. Use
1322+
``LocalConversation.fork(agent=...)`` for agent replacement.
13221323
title: Optional title for the forked conversation.
13231324
tags: Optional tags for the forked conversation.
13241325
reset_metrics: If ``True`` (default), cost/token stats start
@@ -1327,12 +1328,19 @@ def fork(
13271328
Returns:
13281329
A new ``RemoteConversation`` backed by the forked server-side
13291330
conversation.
1331+
1332+
Raises:
1333+
NotImplementedError: If ``agent`` is provided.
13301334
"""
1335+
if agent is not None:
1336+
raise NotImplementedError(
1337+
"Agent replacement is not supported for remote conversation "
1338+
"forks. Use LocalConversation.fork(agent=...) instead."
1339+
)
1340+
13311341
body: dict[str, object] = {"reset_metrics": reset_metrics}
13321342
if conversation_id is not None:
13331343
body["id"] = str(conversation_id)
1334-
if agent is not None:
1335-
body["agent"] = agent.model_dump(mode="json")
13361344
if title is not None:
13371345
body["title"] = title
13381346
if tags is not None:
@@ -1347,13 +1355,10 @@ def fork(
13471355
fork_info = resp.json()
13481356
fork_uuid = uuid.UUID(fork_info["id"])
13491357

1350-
if agent is None:
1351-
agent_cls = type(self.agent)
1352-
fork_agent = agent_cls.model_validate(
1353-
self.agent.model_dump(context={"expose_secrets": True}),
1354-
)
1355-
else:
1356-
fork_agent = agent
1358+
agent_cls = type(self.agent)
1359+
fork_agent = agent_cls.model_validate(
1360+
self.agent.model_dump(context={"expose_secrets": True}),
1361+
)
13571362

13581363
return RemoteConversation(
13591364
agent=fork_agent,

0 commit comments

Comments
 (0)