> ## Documentation Index
> Fetch the complete documentation index at: https://docs.osmosis.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK v0.2 → v0.3 Migration Guide

> Migrate LocalBackend and HarborBackend rollouts from Osmosis SDK v0.2 to v0.3

Osmosis SDK v0.3 contains two independent breaking changes. All rollouts move to a one-execution, one-sample, one-reward contract. Separately, the pre-v0.3 `HarborBackend` is removed and `HarborBackendV2` becomes the new `HarborBackend` with a different constructor and execution model.

<Warning>
  Upgrade the package requirement, workflow, grader, integrations, and backend entrypoint together. Mixing v0.2 rollout code with a v0.3 backend usually fails at runtime.
</Warning>

Use the section that matches your backend:

| Current backend          | Migration path                                                                                            |
| ------------------------ | --------------------------------------------------------------------------------------------------------- |
| `LocalBackend`           | Keep its constructor, then update the shared sample, reward, integration, and routing APIs                |
| Pre-v0.3 `HarborBackend` | Apply the shared API changes, then replace the backend constructor and container execution model          |
| `HarborBackendV2`        | Apply the shared API changes and rename it to `HarborBackend`; its v2 constructor is the v0.3 constructor |

Before changing code:

* Keep the last successful v0.2 evaluation run for comparison.
* Use Python 3.12 or later.
* Recreate the rollout environment and lockfile after changing the SDK requirement.
* Search for removed APIs before and after the migration.

## LocalBackend Users

`LocalBackend` keeps the same keyword-only constructor in v0.3:

```python theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
from osmosis_ai.rollout import LocalBackend

backend = LocalBackend(
    workflow=MyWorkflow,
    workflow_config=my_workflow_config,
    grader=MyGrader,
    grader_config=my_grader_config,
)
```

The migration work is in the workflow, grader, integrations, and any custom protocol code around the backend.

### 1. Upgrade the Package and Select Features

The base distribution contains the CLI and framework-neutral rollout core. Add only the extras your rollout imports:

<CodeGroup>
  ```bash Strands theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
  pip install --upgrade "osmosis-ai[strands]>=0.3,<0.4"
  ```

  ```bash OpenAI Agents theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
  pip install --upgrade "osmosis-ai[openai-agents]>=0.3,<0.4"
  ```

  ```bash Rollout server theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
  pip install --upgrade "osmosis-ai[server]>=0.3,<0.4"
  ```

  ```bash Everything theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
  pip install --upgrade "osmosis-ai[full]>=0.3,<0.4"
  ```
</CodeGroup>

Combine extras when needed. For example, a server entrypoint using Strands should declare:

```toml theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
[project]
dependencies = [
    "osmosis-ai[server,strands]>=0.3,<0.4",
]
```

Other extras include `openai-agents`, `harbor`, `rubric`, and `parquet`.

### 2. Move from Many Samples to One Sample

Each workflow execution now produces at most one `RolloutSample`, and its grader assigns one scalar reward.

| v0.2                                       | v0.3                            | Action                                                     |
| ------------------------------------------ | ------------------------------- | ---------------------------------------------------------- |
| `GraderContext.samples`                    | `GraderContext.sample`          | Read and score the single sample                           |
| `ctx.set_sample_reward(sample_id, reward)` | `ctx.set_reward(reward)`        | Remove the sample ID argument                              |
| `register_sample_source(name, source)`     | `set_sample_source(source)`     | Register exactly one source                                |
| `get_samples()`                            | `get_sample()`                  | Return one `RolloutSample` or `None`                       |
| `RolloutSample.id`                         | Removed                         | Use the rollout identity supplied by the execution URLs    |
| `MultiTurnMode`                            | Removed                         | Keep the conversation history inside the single sample     |
| Multiple registered agents or sessions     | One registered agent or session | Run independent candidates as separate workflow executions |

Update graders from a loop over samples to one explicit sample check:

<CodeGroup>
  ```python v0.2 theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
  class ExactMatchGrader(Grader):
      async def grade(self, ctx: GraderContext) -> None:
          for sample_id, sample in ctx.samples.items():
              answer = last_text(sample)
              reward = 1.0 if answer == ctx.label else 0.0
              ctx.set_sample_reward(sample_id, reward)
  ```

  ```python v0.3 theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
  class ExactMatchGrader(Grader):
      async def grade(self, ctx: GraderContext) -> None:
          if ctx.sample is None:
              raise ValueError("workflow produced no sample")
          answer = last_text(ctx.sample)
          reward = 1.0 if answer == ctx.label else 0.0
          ctx.set_reward(reward)
  ```
</CodeGroup>

`set_reward()` raises `ValueError` when the workflow produced no sample. Raising an explicit error before scoring generally gives a clearer evaluation failure.

### 3. Register One Agent or Session per Execution

Construct exactly one registered `OsmosisStrandsAgent` or `OsmosisMemorySession` inside `AgentWorkflow.run()`. A second registration raises `ValueError`.

For OpenAI Agents, the v0.3 session has no name or sample-ID argument:

```python theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
class OpenAIWorkflow(AgentWorkflow):
    async def run(self, ctx: AgentWorkflowContext) -> None:
        agent = OsmosisAgent(
            name="assistant",
            instructions="Answer the user's request clearly.",
            model=OsmosisRolloutModel(),
        )
        session = OsmosisMemorySession()
        await Runner.run(agent, ctx.prompt, session=session)
```

Handoffs and tool calls can remain inside that one agent run. If you need several candidate answers for one prompt, configure evaluation or training to execute the workflow several times.

### 4. Update Custom Sample Sources

Built-in integrations already use the v0.3 API. A custom integration must implement the singular source contract:

```python theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
class MySampleSource(SampleSource):
    async def get_sample(self) -> RolloutSample:
        return RolloutSample(messages=self.messages)


rollout_ctx.set_sample_source(MySampleSource(messages))
sample = await rollout_ctx.get_sample()
```

Do not set an `id` on `RolloutSample`. To control the normalized ATIF transcript, set `trajectory_messages`; setting it to `None` disables trajectory persistence for that sample.

### 5. Update Custom Routing and Backend Adapters

Skip this step if you only use the built-in integrations and `LocalBackend`.

Treat the chat-completions and callback URLs supplied for an execution as opaque, rollout-scoped endpoints. Do not append a rollout ID or attach the removed `x-sample-id` and `x-rollout-id` routing headers.

Custom backends return one `sample` in `ExecutionResult`. If you read the container exchange files directly, update readers to the singular file names and payloads:

```json sample.json theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
{
  "messages": [],
  "reward": null,
  "remove_sample": false,
  "metrics": {},
  "extra_fields": {}
}
```

```json reward.json theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
{
  "reward": 1.0
}
```

### 6. Verify a LocalBackend Migration

1. Confirm that removed APIs no longer appear in the rollout:

   ```bash theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
   rg "ctx\.samples|set_sample_reward|register_sample_source|get_samples|MultiTurnMode|x-sample-id|x-rollout-id" rollouts
   ```

2. Recreate the rollout environment so its lockfile contains v0.3 and the selected extras.

3. Run the workflow locally and confirm that each successful execution produces one sample.

4. Submit an evaluation run and confirm that each graded sample has one scalar reward.

5. Compare rewards and final messages with the last successful v0.2 evaluation before submitting training.

### Common LocalBackend Problems

<AccordionGroup>
  <Accordion title="A second agent or session raises a registration error">
    The workflow execution already registered its sample source. Reuse one agent or session for the conversation, or move independent candidates into separate workflow executions.
  </Accordion>

  <Accordion title="The grader has no sample">
    Construct the supported agent or session inside `AgentWorkflow.run()`. Objects created at module import time cannot register with the active rollout context.
  </Accordion>

  <Accordion title="An optional integration cannot be imported">
    Install the matching extra in the rollout-local environment and regenerate its lockfile.
  </Accordion>

  <Accordion title="Model calls reach the wrong rollout">
    Pass the supplied chat-completions URL directly to the integration. Remove code that reconstructs the URL, adds a rollout path segment, or attaches legacy routing headers.
  </Accordion>
</AccordionGroup>

## HarborBackend Users

Harbor users must first apply the shared workflow, grader, sample-source, and routing changes in [LocalBackend Users](#localbackend-users). Then migrate the Harbor class and constructor.

<Warning>
  The name `HarborBackend` still imports in v0.3, but it now refers to the implementation previously called `HarborBackendV2`. A pre-v0.3 call using `task_dir`, `user_code_dir`, or `workflow` raises `TypeError`; there is no legacy compatibility mode.
</Warning>

### 1. Upgrade the Package and Import

Install the v0.3 Harbor and server features:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
pip install --upgrade "osmosis-ai[server,harbor]>=0.3,<0.4"
```

<Warning>
  Install `osmosis-ai[harbor]`, not Harbor's `skypilot` extra. The managed rollout runtime supplies the compatible SkyPilot SDK.
</Warning>

Use the Harbor submodule import:

```python theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
from osmosis_ai.rollout.backend.harbor import HarborBackend
```

If you already use `HarborBackendV2`, change only the class name and keep its v2 constructor arguments:

```python theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
# v0.2 preview API
from osmosis_ai.rollout.backend.harbor import HarborBackendV2

# v0.3
from osmosis_ai.rollout.backend.harbor import HarborBackend
```

`HarborBackendV2` is not retained as an alias.

### 2. Replace the Pre-v0.3 Constructor

The old backend mounted your source tree and SDK into a task environment. The v0.3 backend packages the workflow project into a wheel and installs it inside the task container.

<CodeGroup>
  ```python Pre-v0.3 theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
  from pathlib import Path

  from osmosis_ai.rollout.backend.harbor import HarborBackend

  backend = HarborBackend(
      orchestrator=trial_queue,
      task_dir=Path("tasks/my-task"),
      user_code_dir=Path("."),
      workflow=MyWorkflow,
      workflow_config=my_workflow_config,
      grader=MyGrader,
      grader_config=my_grader_config,
      cleanup_successful_trials=True,
  )
  ```

  ```python v0.3 theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
  from pathlib import Path

  from osmosis_ai.rollout.backend.harbor import HarborBackend

  backend = HarborBackend(
      orchestrator=trial_queue,
      tasks_dir=Path("tasks/my-task"),
      task_mode="template",
      agent=MyWorkflow,
      workflow_config=my_workflow_config,
      grader=MyGrader,
      grader_config=my_grader_config,
      code_dir=Path("."),
      cleanup_successful_trials=True,
  )
  ```
</CodeGroup>

Migrate every old constructor parameter with this table:

| Pre-v0.3 parameter           | v0.3 replacement              | Migration notes                                                                                      |
| ---------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------- |
| `orchestrator=`              | `orchestrator=`               | Carries over unchanged                                                                               |
| `task_dir=`                  | `tasks_dir=` and `task_mode=` | Use `"template"` for one reusable task or `"dataset"` for a directory of tasks                       |
| `user_code_dir=`             | `code_dir=` or `bundle=`      | Point `code_dir` at a package project with `pyproject.toml`, or provide a prebuilt bundle wheel      |
| `workflow=`                  | `agent=`                      | Pass an `AgentWorkflow` class/import path or a registered native Harbor agent name                   |
| `workflow_config=`           | `workflow_config=`            | Carries over; it is packaged with a workflow agent                                                   |
| `grader=`                    | `grader=`                     | Carries over; the grader is packaged and runs as the Harbor verifier                                 |
| `grader_config=`             | `grader_config=`              | Carries over unchanged                                                                               |
| `trials_dir=`                | `trials_dir=`                 | Carries over, but omitting it now uses a backend-specific temporary root instead of `Path("trials")` |
| `custom_tests_dir=`          | Removed                       | Put tests under each task's `tests/`, or pass an Osmosis `grader=`                                   |
| `environment_config=`        | `environment_config=`         | Carries over unchanged                                                                               |
| `prebuild_local_image=`      | Removed                       | Harbor handles image caching; call `prewarm()` or `prewarm_lifespan()` before serving                |
| `symlink_environment=`       | Removed                       | Each rollout materializes a task copy and relies on Harbor image caching                             |
| `cleanup_successful_trials=` | `cleanup_successful_trials=`  | Carries over unchanged                                                                               |

Remove the private `_sdk_source_dir` argument if your harness used it.

The v0.3 constructor also adds these controls:

| New parameter               | Use                                                                       |
| --------------------------- | ------------------------------------------------------------------------- |
| `native_agent_kwargs`       | Configure a registered native Harbor agent                                |
| `model_name`                | Select the native agent model; defaults to `openai/osmosis-rollout`       |
| `bundle`                    | Reuse a prebuilt Osmosis bundle wheel instead of building from `code_dir` |
| `patch_dockerfile_with_sdk` | Control preinstallation of bundle dependencies in the task image          |
| `agent_setup_timeout_sec`   | Limit Harbor agent setup time                                             |
| `max_queue_depth`           | Bound queued rollouts and enable `429` admission control                  |

### 3. Choose a Task Mode

For the closest replacement of the old single `task_dir`, use template mode:

```python theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
backend = HarborBackend(
    orchestrator=trial_queue,
    tasks_dir=Path("tasks/my-task"),
    task_mode="template",
    agent=MyWorkflow,
)
```

The request prompt replaces `instruction.md` in a per-rollout copy of that task.

Use dataset mode when `tasks_dir` contains one directory per task:

```python theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
backend = HarborBackend(
    orchestrator=trial_queue,
    tasks_dir=Path("tasks"),
    task_mode="dataset",
    agent=MyWorkflow,
)
```

Each request must set `metadata["harbor_task_id"]`; the selected task keeps its own `instruction.md`.

For either mode, `metadata["harbor_task"]` can select a per-rollout local path, Harbor registry package such as `"org/name@ref"`, or Git task. Git tasks also set `metadata["git_url"]` and should pin `metadata["git_commit_id"]`.

If the old backend used `custom_tests_dir`, move those tests into each task:

```text theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
tasks/my-task/
├── instruction.md
├── environment/
│   └── Dockerfile
└── tests/
    └── test.sh
```

Then pass `grader=None` to use the task-native verifier.

### 4. Package Workflow Code Instead of Mounting It

For an `AgentWorkflow`, the backend builds a wheel from `code_dir` and installs it in the container at trial start. The directory must contain `pyproject.toml` and one importable top-level Python package. When `code_dir` is omitted, the backend tries to locate the project containing the workflow class.

Use `bundle=` when your build system creates the Osmosis bundle wheel ahead of time. Do not pass both paths expecting them to be merged; a supplied bundle is used directly.

The old `HarborAgentWorkflowContext.environment` adapter is gone. The workflow itself now runs inside the task container, receives a standard `AgentWorkflowContext`, and accesses files or processes through normal Python APIs:

```python theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
import asyncio

from osmosis_ai.rollout import AgentWorkflow, AgentWorkflowContext


class HarborWorkflow(AgentWorkflow):
    async def run(self, ctx: AgentWorkflowContext) -> None:
        result = await asyncio.create_subprocess_exec(
            "python",
            "/workspace/run_task.py",
        )
        await result.wait()
```

`OsmosisInstalledAgent` is also removed. Do not instantiate or subclass it; pass an `AgentWorkflow` through `agent=`, or select a registered native Harbor agent name.

### 5. Choose an Agent and Reward Source

`agent=` accepts an `AgentWorkflow` class/import path or one of the registered native names: `"terminus-2"`, `"mini-swe-agent"`, and `"oracle"`.

<Note>
  `"oracle"` runs a reference solution to validate a dataset or verifier. It does not emit a model trajectory and must not be used for training.
</Note>

Choose one reward path:

| Configuration     | Reward source                                          |
| ----------------- | ------------------------------------------------------ |
| `grader=MyGrader` | The bundled Osmosis grader runs as the Harbor verifier |
| `grader=None`     | The task's own `tests/` produce the reward             |

### 6. Add Prewarming and Lifecycle Controls

Prewarm the selected task images and agent setup before the rollout server accepts traffic:

```python theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
from osmosis_ai.rollout.server import create_rollout_server

app = create_rollout_server(
    backend=backend,
    lifespan=backend.prewarm_lifespan(),
)
```

Dataset mode requires explicit task IDs:

```python theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
app = create_rollout_server(
    backend=backend,
    lifespan=backend.prewarm_lifespan(["task-a", "task-b"]),
)
```

If you set `max_queue_depth`, `POST /rollout` returns `429` with `Retry-After: 5` when the queue is full. v0.3 also exposes:

* `GET /rollout/{rollout_id}/status`
* `POST /rollout/cancel`
* `HarborBackend.rollout_status()`
* `HarborBackend.cancel_rollouts()`

### 7. Verify a HarborBackend Migration

1. Confirm that the old class and constructor keywords no longer appear:

   ```bash theme={"theme":{"light":"github-light","dark":"github-dark"},"languages":{"custom":["/languages/cli.json"]}}
   rg "HarborBackendV2|HarborAgentWorkflowContext|OsmosisInstalledAgent|task_dir=|user_code_dir=|workflow=|custom_tests_dir=|prebuild_local_image=|symlink_environment=" rollouts
   ```

2. Confirm that `tasks_dir` points to a valid template task or dataset root.

3. Build the workflow bundle and fix any package-layout or missing-dependency errors.

4. Run `await backend.prewarm()` for template mode, or pass task IDs in dataset mode.

5. Submit one rollout and check the workflow sample, reward source, Harbor logs, and archived artifacts.

6. Exercise status and cancellation if the calling controller depends on them.

7. Run an evaluation and compare rewards and final messages with the last successful v0.2 run before training.

### Common HarborBackend Problems

<AccordionGroup>
  <Accordion title="HarborBackend rejects task_dir, user_code_dir, or workflow">
    The import resolves to the v0.3 backend, but the call still uses the removed constructor. Replace all stale parameters with the table above.
  </Accordion>

  <Accordion title="The workflow project cannot be bundled">
    Point `code_dir` at the directory containing `pyproject.toml` and one importable package. Ensure the workflow and grader can be addressed as import paths from that package.
  </Accordion>

  <Accordion title="A dataset-mode request cannot find its task">
    Set `metadata["harbor_task_id"]` to a directory beneath `tasks_dir`, or set `metadata["harbor_task"]` to a supported local, package, or Git reference.
  </Accordion>

  <Accordion title="The trial finishes without a reward">
    Pass an Osmosis `grader`, or set `grader=None` and confirm that the selected task contains a working `tests/` verifier.
  </Accordion>

  <Accordion title="Rollout code expects ctx.environment">
    Remove the legacy adapter calls. The workflow now runs inside the container, so use normal filesystem, subprocess, and network APIs.
  </Accordion>
</AccordionGroup>

## Related Resources

* [Execution Backends](/cli/rollout/execution-backends)
* [Building AgentWorkflows](/cli/rollout/agent-workflows)
* [Building Graders](/cli/rollout/graders)
* [Changelog](/changelog)
