diff --git a/docs.json b/docs.json
index 62905d8d..4337398f 100644
--- a/docs.json
+++ b/docs.json
@@ -250,6 +250,7 @@
{
"group": "Conversation Features",
"pages": [
+ "sdk/guides/convo-fork",
"sdk/guides/convo-pause-and-resume",
"sdk/guides/convo-custom-visualizer",
"sdk/guides/convo-send-message-while-running",
diff --git a/sdk/guides/convo-fork.mdx b/sdk/guides/convo-fork.mdx
new file mode 100644
index 00000000..bdec0e32
--- /dev/null
+++ b/sdk/guides/convo-fork.mdx
@@ -0,0 +1,269 @@
+---
+title: Fork a Conversation
+description: Branch off an existing conversation for follow-up exploration without contaminating the original.
+---
+
+import RunExampleCode from "/sdk/shared-snippets/how-to-run-example.mdx";
+
+> A ready-to-run example is available [here](#ready-to-run-example)!
+
+## Overview
+
+`Conversation.fork()` deep-copies a conversation — events, agent config, workspace metadata — into a new conversation with its own ID. The fork starts in `idle` status and retains the full event memory of the source, so calling `run()` picks up right where the original left off.
+
+**Use cases:**
+- **CI debugging** — an agent produced a wrong patch; fork to debug without losing the original run's audit trail
+- **A/B testing** — fork at a given turn, change one variable, compare downstream outcomes
+- **Tool-change** — fork and swap in a different agent with new tools mid-conversation
+
+## Basic Usage
+
+### Create a fork
+
+```python icon="python" focus={6} wrap
+source = Conversation(agent=agent, workspace=workspace)
+source.send_message("Analyse the sales report.")
+source.run()
+
+# Fork the conversation with a title
+fork = source.fork(title="Follow-up exploration")
+
+# The fork has the same events — agent remembers the full history
+fork.send_message("Now focus on the EMEA region.")
+fork.run() # Continues from the source's state
+```
+
+### Source stays immutable
+
+Forking deep-copies events and state. Anything you do on the fork never touches the source:
+
+```python icon="python" wrap
+source_events_before = len(source.state.events)
+
+fork = source.fork()
+fork.send_message("Extra question")
+
+assert len(source.state.events) == source_events_before # unchanged
+```
+
+### Fork with a different agent
+
+Swap the agent on fork — useful for A/B testing models or adding/removing tools:
+
+```python icon="python" focus={8-11} wrap
+alt_llm = LLM(model="openai/gpt-4o", api_key=api_key, usage_id="alt")
+alt_agent = Agent(llm=alt_llm, tools=[Tool(name=TerminalTool.name)])
+
+fork = source.fork(
+ agent=alt_agent,
+ title="GPT-4o experiment",
+ tags={"variant": "B"},
+)
+fork.run() # Same history, different model
+```
+
+### Tags and metadata
+
+Forks support `title` and arbitrary `tags` for organization:
+
+```python icon="python" wrap
+fork = source.fork(
+ title="Debug investigation",
+ tags={"purpose": "debugging", "triggered_by": "ci-pipeline"},
+)
+
+print(fork.state.tags)
+# {'title': 'Debug investigation', 'purpose': 'debugging', 'triggered_by': 'ci-pipeline'}
+```
+
+### Metrics reset
+
+By default, cost/token stats start fresh on the fork. Pass `reset_metrics=False` to preserve them:
+
+```python icon="python" wrap
+# Cost starts at 0 on the fork (default)
+fork_fresh = source.fork()
+
+# Cost carries over from source
+fork_with_history = source.fork(reset_metrics=False)
+```
+
+## API Reference
+
+```python icon="python" wrap
+def fork(
+ self,
+ *,
+ conversation_id: ConversationID | None = None, # auto-generated if None
+ agent: AgentBase | None = None, # deep-copy of source agent if None
+ title: str | None = None, # sets tags["title"]
+ tags: dict[str, str] | None = None, # arbitrary metadata
+ reset_metrics: bool = True, # cost/tokens start fresh
+) -> Conversation:
+```
+
+| Parameter | Default | Description |
+|-----------|---------|-------------|
+| `conversation_id` | auto-generated UUID | ID for the forked conversation |
+| `agent` | deep-copy of source | Agent for the fork (swap model, tools, etc.) |
+| `title` | `None` | Sets `tags["title"]` on the fork |
+| `tags` | `None` | Arbitrary key-value metadata |
+| `reset_metrics` | `True` | Whether cost/token stats start at zero |
+
+**Returns:** A new `Conversation` with the same event history but independent state.
+
+## What Gets Copied
+
+| Component | Behavior |
+|-----------|----------|
+| **Events** | Deep-copied; source is never modified |
+| **Agent** | Deep-copied by default, or replaced via the `agent` kwarg |
+| **Workspace** | Shared (same working directory) |
+| **Confirmation policy** | Copied from source |
+| **Security analyzer** | Copied from source |
+| **Stats / Metrics** | Reset by default (`reset_metrics=True`) |
+| **Execution status** | Always `idle` on the fork |
+| **Conversation ID** | New UUID (or explicit via `conversation_id`) |
+
+## Agent-Server REST Endpoint
+
+When using the [agent-server](/sdk/guides/agent-server/overview), forks are available via REST:
+
+```bash icon="terminal"
+POST /api/conversations/{id}/fork
+```
+
+**Request body** (all fields optional):
+
+```json
+{
+ "id": "custom-uuid-or-null",
+ "title": "Debug investigation",
+ "tags": {"purpose": "debugging"},
+ "reset_metrics": true
+}
+```
+
+**Response:** Standard `ConversationInfo` for the newly created fork.
+
+## Ready-to-run Example
+
+
+This example is available on GitHub: [examples/01_standalone_sdk/48_conversation_fork.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/48_conversation_fork.py)
+
+
+```python icon="python" expandable examples/01_standalone_sdk/48_conversation_fork.py
+"""Fork a conversation to branch off for follow-up exploration.
+
+``Conversation.fork()`` deep-copies a conversation — events, agent config,
+workspace metadata — into a new conversation with its own ID. The fork
+starts in ``idle`` status and retains full event memory of the source, so
+calling ``run()`` picks up right where the original left off.
+
+Use cases:
+ - CI agents that produced a wrong patch — engineer forks to debug
+ without losing the original run's audit trail
+ - A/B-testing prompts — fork at a given turn, change one variable,
+ compare downstream
+ - Swapping tools mid-conversation (fork-on-tool-change)
+"""
+
+import os
+
+from openhands.sdk import LLM, Agent, Conversation, Tool
+from openhands.tools.terminal import TerminalTool
+
+
+# -----------------------------------------------------------------
+# Setup
+# -----------------------------------------------------------------
+llm = LLM(
+ model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"),
+ api_key=os.getenv("LLM_API_KEY"),
+ base_url=os.getenv("LLM_BASE_URL", None),
+)
+
+agent = Agent(llm=llm, tools=[Tool(name=TerminalTool.name)])
+cwd = os.getcwd()
+
+# =================================================================
+# 1. Run the source conversation
+# =================================================================
+source = Conversation(agent=agent, workspace=cwd)
+source.send_message("Run `echo hello-from-source` in the terminal.")
+source.run()
+
+print("=" * 64)
+print(" Conversation.fork() — SDK Example")
+print("=" * 64)
+print(f"\nSource conversation ID : {source.id}")
+print(f"Source events count : {len(source.state.events)}")
+
+# =================================================================
+# 2. Fork and continue independently
+# =================================================================
+fork = source.fork(title="Follow-up fork")
+source_event_count = len(source.state.events)
+
+print("\n--- Fork created ---")
+print(f"Fork ID : {fork.id}")
+print(f"Fork events (copied) : {len(fork.state.events)}")
+print(f"Fork title : {fork.state.tags.get('title')}")
+
+assert fork.id != source.id
+assert len(fork.state.events) == source_event_count
+
+fork.send_message("Now run `echo hello-from-fork` in the terminal.")
+fork.run()
+
+# Source is untouched
+assert len(source.state.events) == source_event_count
+print("\n--- After running fork ---")
+print(f"Source events (unchanged): {source_event_count}")
+print(f"Fork events (grew) : {len(fork.state.events)}")
+
+# =================================================================
+# 3. Fork with a different agent (tool-change / A/B testing)
+# =================================================================
+alt_llm = LLM(
+ model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"),
+ api_key=os.getenv("LLM_API_KEY"),
+ base_url=os.getenv("LLM_BASE_URL", None),
+ usage_id="alt",
+)
+alt_agent = Agent(llm=alt_llm, tools=[Tool(name=TerminalTool.name)])
+
+fork_alt = source.fork(
+ agent=alt_agent,
+ title="Tool-change experiment",
+ tags={"purpose": "a/b-test"},
+)
+
+print("\n--- Fork with alternate agent ---")
+print(f"Fork ID : {fork_alt.id}")
+print(f"Fork tags : {dict(fork_alt.state.tags)}")
+
+fork_alt.send_message("What command did you run earlier? Just tell me, no tools.")
+fork_alt.run()
+
+print(f"Fork events : {len(fork_alt.state.events)}")
+
+# =================================================================
+# Summary
+# =================================================================
+print(f"\n{'=' * 64}")
+print("All done — fork() works end-to-end.")
+print("=" * 64)
+
+# Report cost
+cost = llm.metrics.accumulated_cost + alt_llm.metrics.accumulated_cost
+print(f"EXAMPLE_COST: {cost}")
+```
+
+
+
+## Next Steps
+
+- **[Persistence](/sdk/guides/convo-persistence)** — Save and restore conversation state
+- **[Pause and Resume](/sdk/guides/convo-pause-and-resume)** — Control execution flow
+- **[Agent Server](/sdk/guides/agent-server/overview)** — Deploy agents with the REST API