Skip to content

[1/5][rollout] feat: harbor backend v2 - #272

Merged
mathewjhan merged 38 commits into
mainfrom
feat/harbor-backend-v2
Aug 6, 2026
Merged

[1/5][rollout] feat: harbor backend v2#272
mathewjhan merged 38 commits into
mainfrom
feat/harbor-backend-v2

Conversation

@mathewjhan

@mathewjhan mathewjhan commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

What

Why

How to Test

Checklist

  • PR title follows [module] type: description format
  • Appropriate labels added (e.g. enhancement, bug, breaking)
  • ruff check . and ruff format --check . pass
  • pyright osmosis_ai/ passes
  • pytest passes (new tests added if applicable)
  • Public API changes are documented
  • No secrets or credentials included

Summary by cubic

Add HarborBackendV2 to run Harbor tasks with pure task images via bundled workflows or native Harbor agents, plus admission control, rollout status polling, cancellation, and artifact handling. Native Harbor tasks use Harbor’s validator when running native agents.

  • New Features

    • HarborBackendV2: bundled workflows or native agents (terminus-2, mini-swe-agent, oracle); template/dataset modes; per‑phase timing with failure diagnostics; secrets redacted; diagnostics returned via extra_fields on results.
    • Bundling (osmosis_ai.packaging): builds a wheel from a pyproject.toml harness, injects agent/grader console scripts, caches by content hash in the user cache dir via platformdirs, and exposes inspect_bundle/project_dir_for.
    • Container I/O: ContainerInput/ContainerResult plus in‑container runner.agent_main/runner.grader_main; extracts messages from trajectories, writes reward files for Harbor, and adds AgentWorkflowOutput.
    • Task materialization: HarborTask + TaskMode copy per rollout, write instruction.md, patch Dockerfiles to install SDK deps into an isolated venv (/opt/osmosis/venv) bootstrapped with uv (fallback to pip), and generate tests/test.sh to install a grader when needed.
    • Native agents: pass model endpoint via env/kwargs per agent; support native_agent_kwargs for per‑rollout agent settings; use Harbor’s native validator; bundled workflows run from the venv.
    • HarborBackend: content‑addressed image tags to reuse builds; new exports in osmosis_ai.rollout.backend.harbor.__init__.
    • Server: admission control with has_capacity() (429 + Retry-After when full), GET /rollout/{id}/status for lifecycle and terminal states, POST /rollout/cancel (ids/prefix/all), and POST /rollout returns 202 when accepted and queued.
    • Artifacts/retention: host‑side trial artifact relocation and a simple TTL cache for recent rollout status.
    • Extras: unit tests for packaging, backend, server, and container runner; benchmarks/container_lifecycle for cold/warm throughput.
  • Migration

    • Use HarborBackendV2 with tasks_dir; choose a native agent by name or provide a workflow harness (code_dir or a prebuilt bundle).
    • Ensure the harness is a Python project with a pyproject.toml; the bundler creates <package>-agent/<package>-grade scripts automatically.
    • Bundles install into /opt/osmosis/venv; images only need Python. For dataset runs, set task_mode="dataset" and include harbor_task_id in request metadata.
    • For native agents, pass per‑rollout settings via native_agent_kwargs.
    • Clients should handle 202 from POST /rollout, 429 with Retry-After, may call POST /rollout/cancel, and can poll GET /rollout/{id}/status for lifecycle and terminal states.

Written for commit 8bf241a. Summary will update on new commits.

Review in cubic

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.42675% with 96 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
osmosis_ai/rollout/container/runner.py 31.42% 72 Missing ⚠️
osmosis_ai/packaging.py 89.07% 6 Missing and 7 partials ⚠️
osmosis_ai/rollout/container/trajectories.py 30.76% 9 Missing ⚠️
osmosis_ai/rollout/container/files.py 94.44% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@JoyboyBrian JoyboyBrian changed the title WIP: harbor backend v2 [rollout] feat: harbor backend v2 Aug 5, 2026
@github-actions github-actions Bot added enhancement New feature or request rollout Remote Rollout module labels Aug 5, 2026
mathewjhan added a commit that referenced this pull request Aug 6, 2026
…ntainer runner (#284)

Part of splitting #272 into reviewable pieces (2/6).

## What this adds

The data types and the in-container code that let a user's agent
workflow run inside a task container and report results back to the
host.

- `ContainerInput` / `ContainerResult` (`container/files.py`): the two
JSON files exchanged with the container. The host stages
`container_input.json` (rollout id, prompt, metadata, chat-completions
URL, API key) before the agent starts; the agent phase writes
`container_result.json` (status, error, workflow output) when it ends.
`write_reward` writes the reward file at the path Harbor's verifier
reads (`/logs/verifier/reward.json`).
- `AgentWorkflowOutput` (`types/output.py`): what a workflow's `run()`
may return — one or more named message histories plus metrics. Returning
nothing is also valid; the runner then collects the conversation
recorded at the chat-completions endpoint.
- `RolloutStatus` (`types/sample.py`): one status vocabulary used
everywhere — `queued`, `running`, `grading`, `success`, `failure`,
`cancelled`, `unknown`. `RolloutSample` gains `extra_fields` for
structured diagnostics.
- The runner (`container/runner.py`): the entrypoints that the generated
bundle scripts call inside the container. `agent_main` reads the input
file, runs the workflow, writes the result file, and saves the
conversation as an ATIF `trajectory.json` next to the agent logs — the
same location and format native Harbor agents use, so tools that read
trajectories work on both. `grader_main` loads the agent's messages
(from the result file, or from a trajectory file when the agent was not
SDK code), runs the user's grader, and writes the reward. The grader
reads its input from `tests/` first: that copy may carry the
ground-truth label, while the agent-phase copy has the label removed so
the model can never read the answer.
- `messages_from_trajectory` (`container/trajectories.py`): converts a
trajectory document (ATIF steps, or a raw messages list from a native
harness) into plain chat messages.

## Example

A workflow and grader written against these types:

```python
class MyWorkflow(AgentWorkflow):
    async def run(self, ctx: AgentWorkflowContext) -> None:
        agent = StrandsAgent(model=ctx.config.model, messages=ctx.prompt)
        await agent.invoke_async()
        # returning None is fine: the recorded conversation becomes the sample

class MyGrader(Grader):
    async def grade(self, ctx: GraderContext) -> None:
        answer = ctx.sample.messages[-1]["content"]
        ctx.set_reward(1.0 if answer.strip() == ctx.label else 0.0)
```

Inside the container, the bundle's console scripts call
`agent_main(MyWorkflow, config)` and `grader_main(MyGrader, config)`;
everything else in this PR is the plumbing those two calls rely on.

Co-authored-by: Cursor <cursoragent@cursor.com>
mathewjhan added a commit that referenced this pull request Aug 6, 2026
…dles (#285)

Part of splitting #272 into reviewable pieces (3/6).

## What this adds

`osmosis_ai/packaging.py`: builds a standard Python wheel from a user's
rollout project so the project can be installed inside a task container
with one `pip install`.

- `build_bundle(project_dir, workflow=..., grader=...)` produces a wheel
containing the project's package plus a generated `bundle_main.py` shim.
The shim imports the user's classes directly (`from my_harness.solver
import MyWorkflow`) and exposes two console scripts, `<package>-agent`
and `<package>-grade`, which call the runner entrypoints from #284.
Nothing is resolved dynamically at runtime; the class binding happens at
build time.
- Wheels are cached by a content hash of the project files under the
user cache directory (`platformdirs`), so rebuilding an unchanged
project is free.
- `inspect_bundle(wheel)` reads the wheel's metadata with
`importlib.metadata` and returns the declared dependencies (keeping
environment markers, dropping extras-gated entries) plus the two script
names. The Harbor backend (next in the stack) uses this list to
pre-install dependencies into the task image.

Also includes the `bench_harness` fixture project the packaging tests
build against, and the `platformdirs` dependency.

## Example

```python
from osmosis_ai.packaging import build_bundle, inspect_bundle

wheel = build_bundle(
    Path("my_rollout_project"),
    workflow="my_harness.solver:MyWorkflow",
    grader="my_harness.grade:MyGrader",
)
info = inspect_bundle(wheel)
info.agent_script    # "my-harness-agent"
info.requirements    # ["strands-agents>=1.0", "httpx>=0.27", ...]
```

Inside a container, `pip install my_harness-0.1.0-py3-none-any.whl`
followed by running `my-harness-agent` executes the user's workflow with
no other setup.

Co-authored-by: Cursor <cursoragent@cursor.com>
mathewjhan added a commit that referenced this pull request Aug 6, 2026
Part of splitting #272 into reviewable pieces (4/6). Builds on #284
(container contract) and #285 (packaging).

## What this adds

`HarborBackendV2`: runs each rollout as a Harbor trial. The agent can be
either a user workflow (packaged into a wheel and installed in the
container at trial start) or a registered native Harbor agent
(`terminus-2`, `mini-swe-agent`, `oracle`) with the rollout endpoint
injected into its environment.

How a rollout flows through it:

1. **Task selection** (`tasks.py`): template mode uses one task
directory for every rollout; dataset mode routes by
`metadata["harbor_task_id"]` to a folder under `tasks_dir` (path escapes
rejected); `metadata["harbor_task"]` fetches a task from a local path,
git checkout, or registry package, with per-ref locks so concurrent
rollouts download once.
2. **Materialization**: the task is copied into a per-rollout directory;
the rollout's input file is staged; if the task has no `tests/` and a
grader exists, a `test.sh` is generated that installs and runs the
grader. The ground-truth label is staged only into `tests/`, which
Harbor uploads at verification time — the agent phase cannot read it.
3. **Image preparation**: `patch_dockerfile_with_sdk` appends a block to
the task's Dockerfile that installs a static `uv` binary and creates
`/opt/osmosis/venv` with the bundle's dependencies pre-installed.
Per-trial installs then only add the user's own code (`--no-deps`),
which cuts container startup from minutes to seconds. The patch is
deterministic, so identical tasks keep identical image content hashes
and share builds.
4. **Execution** (`harness_agent.py`): the installed agent uploads the
wheel, installs it into the venv, backfills an empty prompt from the
task's `instruction.md`, runs the agent script, and returns the result
through the trial's agent metadata.
5. **Callbacks**: the workflow-complete callback fires when verification
starts (agent phase over); the grader-complete callback fires at trial
end with the reward parsed from Harbor's verifier result. Callback
delivery failures are logged and never abort trial archival.
6. **Observability and lifecycle**: per-phase timings and failure phases
in every result (`diagnostics.py`), native-agent ATIF parsing with
secret redaction, artifact relocation, `prewarm()` /
`prewarm_lifespan()` to build task images before serving traffic,
`cancel_rollouts(ids | prefix | all)`, `rollout_status()` with terminal
outcomes retained in a `TtlCache` (#283), and admission control via
`max_queue_depth`.

## Example

```python
backend = HarborBackendV2(
    orchestrator=TrialQueue(n_concurrent=100),
    tasks_dir=Path("tasks"),           # 300 task folders
    task_mode="dataset",
    agent=MyWorkflow,                  # or agent="mini-swe-agent"
    workflow_config=my_config,
    environment_config=EnvironmentConfig(type=EnvironmentType.SKYPILOT),
)
app = create_rollout_server(
    backend=backend,
    lifespan=backend.prewarm_lifespan(task_ids=["task-0000"]),
)
```

A trainer then POSTs rollouts with `metadata={"harbor_task_id":
"task-0042"}`; each one runs in its own sandbox and reports back through
the callbacks.

Co-authored-by: Cursor <cursoragent@cursor.com>
mathewjhan added a commit that referenced this pull request Aug 6, 2026
…ncel endpoints (#287)

Part of splitting #272 into reviewable pieces (5/6). Builds on #286
(backend capabilities).

## What this adds

Three server-level behaviors that let a trainer manage load and track
rollouts over plain HTTP:

- **Admission control on `POST /rollout`**: when the backend's queue is
at `max_queue_depth`, the server answers `429` with a `Retry-After`
header instead of accepting work it cannot start. Accepted rollouts
return `202`, which states what actually happens: the rollout is queued
and runs after the response is sent.
- **`GET /rollout/{id}/status`**: reports `queued`, `running`, or
`grading` for live rollouts, and `success` / `failure` / `cancelled`
(with reward and error message) for recently finished ones, retained for
a fixed window. Anything else returns `unknown`. A trainer polls this as
a liveness signal instead of trusting a blind timeout.
- **`POST /rollout/cancel`**: takes exactly one selector — `{"ids":
[...]}`, `{"prefix": "tenant-a::"}`, or `{"all": true}` — and returns a
disposition per rollout (`cancelled_queued`, `cancelled_running`,
`not_found`). Prefix cancellation is what a controller uses to stop
everything belonging to one adapter when it is deregistered. Cancelling
an already-finished rollout returns `not_found`, so the call is safe to
repeat.

## Example

```bash
curl -X POST $SERVER/rollout -d '{"rollout_id": "tenant-a::r1", ...}'
# -> 202 (or 429 + Retry-After: 5 when the queue is full)

curl $SERVER/rollout/tenant-a::r1/status
# -> {"rollout_id": "tenant-a::r1", "status": "running"}

curl -X POST $SERVER/rollout/cancel -d '{"prefix": "tenant-a::"}'
# -> {"dispositions": {"tenant-a::r1": "cancelled_running"}}
```

Co-authored-by: Cursor <cursoragent@cursor.com>
mathewjhan added a commit that referenced this pull request Aug 6, 2026
…288)

Part of splitting #272 into reviewable pieces (6/6). Exercises
everything below it in the stack.

## What this adds

A CLI that measures how long the full rollout lifecycle takes on the
Harbor backend: submit, container build/start, agent run, verification,
teardown. It reports cold and warm timings separately and per-rollout
p50/p95/max, which is how regressions in container startup cost were
found and fixed during development.

It runs against the bundled `bench_harness` project by default (a
minimal workflow + grader), or against any Harbor task folder via
`--tasks-dir`. Rollout ids carry a per-invocation nonce so concurrent
bench runs cannot collide.

## Example

```bash
# default bench task, 3 runs of 5 concurrent rollouts
uv run benchmarks/container_lifecycle/container_lifecycle_bench.py --runs 3 --concurrency 5

# against a real dataset task, without baking the SDK into the image
uv run benchmarks/container_lifecycle/container_lifecycle_bench.py \
    --tasks-dir harbor-datasets/datasets/algotune/algotune-aes-gcm-encryption \
    --runs 3 --concurrency 5 --no-patch-dockerfile-with-sdk
```

Output shows per-run wall time, success counts, rollouts/sec, and
warm-run latency percentiles.

Co-authored-by: Cursor <cursoragent@cursor.com>
@mathewjhan
mathewjhan marked this pull request as ready for review August 6, 2026 22:40
@mathewjhan mathewjhan changed the title [rollout] feat: harbor backend v2 [1/5][rollout] feat: harbor backend v2 Aug 6, 2026
@mathewjhan
mathewjhan merged commit 584ad08 into main Aug 6, 2026
@mathewjhan
mathewjhan deleted the feat/harbor-backend-v2 branch August 6, 2026 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request rollout Remote Rollout module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants