Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,14 @@ jobs:
exit 1
fi
grep -q "1 passed" /tmp/drift.txt

opencollab:
name: OpenCollab integration
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install -e ".[opencollab,dev]"
- run: python -m pytest tests/worker/agents/opencollab -q -m "not slow"
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ worker of step 3. The worker grades
[SWE-rebench V2](https://github.com/SWE-rebench/SWE-rebench-V2) tasks with a port of its official
harness.

OpenCollab Single2 is an optional agent slot; see [OpenCollab setup and RL integration](docs/OPENCOLLAB.md).

## Learn more

| Guide | What it covers |
Expand Down
134 changes: 134 additions & 0 deletions docs/OPENCOLLAB.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# OpenCollab

Sprout can run OpenCollab's Single2 agent through the same worker, sandbox,
checkpoint, search and training-export interfaces as mini-swe-agent. The default
slot remains `mini-swe-agent`. Select `opencollab-base` to run Single2.

## Install

```bash
pip install -e ".[opencollab,harbor,runstore,rl,dev]"
```

The optional extra pins the public OpenCollab commit
`c69a2076f4b54cc97891df432bf81273e3c5ebf1` (SDK version 0.9.3).
It needs Git and network access at installation. The adapter does not modify the
SDK. Without this extra, mini, oracle, nop and the service clients still import
and run normally; SDK-dependent tests skip. CI installs the extra in a separate job.

Use the normal host setup and services from [README](../README.md). Branching
requires a snapshot backend, such as rootless Podman on a btrfs store.

## Run and evaluate

A direct run uses the worker's existing entry point:

```bash
export OPENAI_BASE_URL=http://127.0.0.1:8000/v1
export OPENAI_API_KEY=local
python -m sprout.worker run --slot opencollab-base --model MODEL \
--sandbox-image docker.io/library/python:3.11-slim --cwd /tmp \
--runtime-bin runtime/sprout-runtime --journal runs/opencollab.jsonl \
"Write and test fizzbuzz.py"
```

Evaluation goes through the Run Store and a running worker, as in
[EVALUATION](EVALUATION.md). Add `--slot opencollab-base` to its existing command.
Use the current upstream's fully prepared task images. For older Harbor bundles,
prepare their declared environment setup and standard logging/artifact directories
in the image before scheduling jobs; this adapter does not restore the removed
Harbor trial runner.

The task's workdir is resolved inside the sandbox before the agent starts, and
recorded with its repository root. A branch restores that physical directory,
not the host directory containing its logs.

Single2 receives its native tools (`bash`, `file_read`, `file_write`, `apply_patch`,
`git_diff`, `grep`). These are SDK tools backed by Sprout's `ToolAccess`; the Go
runtime toolset is unchanged. Every environment operation runs in the owned
sandbox, as the image's configured user. File writes use bounded, verified chunks.

## Checkpoints and branches

A recovery point pairs a completed runtime turn's disk snapshot with a
checksummed native transcript prefix and the corresponding SDK session state.
The worker restores the prefix and SDK state before executing the continuation.
A different slot, protocol, checksum or workspace is rejected.

The SDK uses non-streaming Chat Completions through Sprout's metered gateway.
Native context shaping is recorded as the actual replacement context. Recovery
point indexing only admits closed tool-call groups with matching snapshots.
Cancellation waits for an owned runtime command to settle; unknown execution or
checkpoint failure stops the run and remains an infrastructure failure.

SPROUT still chooses branch points and guidance through its existing search and
review jobs. Flat reviewer commands are serialized as the selected parent's
actual command tool. Nested reviewer calls are validated against that parent's
recorded schemas, with unique ids and no reviewer-authored submission.
Both ordinary-content and reasoning-content continuations are supported.

## RL messages

In the RL driver's existing `miles.run_defaults`, select:

```json
{
"slot": "opencollab-base",
"extra": {
"mini": {"environment": {"cwd": "/workspace"}}
}
}
```

`mini.environment.cwd` is the existing worker workspace configuration key,
shared by these slots. The rest of the driver, worker profiles and task grader
configuration follows [RL_DRIVER](RL_DRIVER.md). Set resources, endpoints and
paths for your host. Search uses the current upstream request's `search` field
and its documented variable-size group semantics; this adapter does not pad
successful searches or change their stopping policy.

The worker exports the SDK's actual messages and tool schemas, plus provenance:
restored prefix messages, reviewer-authored assistant turns, and SDK-generated
submission receipts. The existing driver excludes these from assistant loss.
The sampled submission action remains a policy target. Real tool replies stay
paired with their calls. Original task verifiers provide rewards; grader or
transport failure is never fabricated as reward zero.

Miles owns tokenization, assistant token masks, advantages, loss, optimization,
checkpoints and serving-weight synchronization. No trainer is included here.
Use a Miles client compatible with the current upstream driver's search request
and result contract; older fixed-size Ash clients need their upstream update.

## Historical tool compatibility

For external trajectories recorded with `run_command`, `read_file`, `edit_file`,
`write_file`, `get_status` and `submit_patch`, explicitly select
`opencollab_tool_protocol: review50-v1` in the slot's `extra`. Evaluation exposes
`--opencollab-tool-protocol review50-v1`. This compatibility mode still runs the
Single2 session loop. It does not require or distribute the review50 dataset.

Configure the actual checkout as the task workdir or `mini.environment.cwd`.
Compatibility mode exposes that checkout at `/workspace` before the first model
action; it refuses an existing alias pointing elsewhere.
`submit_patch` produces a real diff receipt and records the SDK submission fact;
it terminates through native Single2. The optional `/opt/review50-baseline`
marker names the task's prepared Git baseline; otherwise the diff uses `HEAD`.
Official grading remains separate. `get_status` reports the actual working-tree
state, deadline and enforced model-turn count. Tool calls are counted and have
no numeric budget limit; an absent limit is `null`.

## Validation

```bash
python -m pytest tests/worker/agents/opencollab -q
python -m pytest -q -m "not slow"
./gate/run.sh
python ops/contracts/ci_check.py
```

The integration tests exercise real Single2 sessions against a scripted model
and an isolated shell fixture: multi-tool turns, snapshots, native restoration,
reviewer injection, context shaping, submission, training provenance, large
writes, cancellation and infrastructure failures. They do not claim policy
learning or benchmark performance. GPU, task images and snapshot storage are
external prerequisites for live RL; none is required by ordinary unit tests.
2 changes: 1 addition & 1 deletion docs/RL_DRIVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ return 503. These fields need care:
| --- | --- |
| `driver_token_env` | The variable of the callers' bearer token. `null` turns authentication off. The example config sets `null`, so use it only for local access. |
| `ledger` | Required. A relative path starts from the directory of the config file. |
| `miles.run_defaults` | RunSpec fields for each root rollout. `slot` must be `mini-swe-agent`. `model` applies when the request names none. |
| `miles.run_defaults` | RunSpec fields for each root rollout. `slot` is `mini-swe-agent` or `opencollab-base` ([OpenCollab](OPENCOLLAB.md)). `model` applies when the request names none. |
| `miles.image_resources` | Required: the `cpu` and the `memory_mb` of each sandbox. |
| `miles.tasks` | For each `task_id`, a `grade` job template without `snapshot_id`. The adapter refuses a request for a task without one. |
| `miles.api_key_env` | The variable in the worker's environment that holds the key of the model endpoint. The worker profile's `worker_env` must set it. |
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies = [
[project.optional-dependencies]
# The agent and the trial runner the evaluations pin.
mini = ["mini-swe-agent==2.4.6"]
opencollab = ["opencollab @ git+https://github.com/RISE-X-Lab/OpenCollab.git@c69a2076f4b54cc97891df432bf81273e3c5ebf1"]
harbor = ["harbor==0.22.0"]
# The Run Store queue and the RL driver.
runstore = ["psycopg2-binary>=2.9,<3", "fastapi>=0.110,<1", "uvicorn>=0.29,<1"]
Expand All @@ -29,6 +30,9 @@ docs = ["fonttools>=4", "uharfbuzz>=0.39"]
[project.urls]
Homepage = "https://github.com/dreamyang-liu/sprout"

[tool.hatch.metadata]
allow-direct-references = true

[tool.hatch.build.targets.wheel]
packages = ["src/sprout", "src/sprout_sandbox"]

Expand Down Expand Up @@ -61,6 +65,10 @@ warn_unreachable = true
module = ["harbor.*"]
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = ["opencollab.*"]
ignore_missing_imports = true

[tool.pytest.ini_options]
pythonpath = ["src", "."]
testpaths = ["tests"]
Expand Down
122 changes: 122 additions & 0 deletions resources/opencollab/review50_protocol.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
{
"name": "review50-v1",
"tools": [
{
"type": "function",
"function": {
"name": "run_command",
"description": "Execute a shell command inside the repository sandbox.\n\nYou can run any shell command (Python scripts, tests, git commands, etc.).\nThe command runs in the repository workspace. No network access.\n\nArgs:\n command: Shell command to execute (e.g., \"pytest tests/\", \"python3 script.py\").",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string"
}
},
"required": [
"command"
]
}
}
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read the contents of a file from the repository workspace.\n\nSupports reading specific line ranges using start_line and end_line (1-indexed).\nTo avoid overwhelming the context window, large files are automatically truncated\nto a maximum number of lines (default: 150 lines).\n\nArgs:\n filepath: Relative path within the workspace directory (e.g., \"fastapi/main.py\", \"pyproject.toml\").\n start_line: Optional starting line number (1-indexed, inclusive).\n end_line: Optional ending line number (1-indexed, inclusive).\n\nReturns:\n Dictionary containing file content, line range, and truncation status.",
"parameters": {
"type": "object",
"properties": {
"filepath": {
"type": "string"
},
"start_line": {
"type": "integer"
},
"end_line": {
"type": "integer"
}
},
"required": [
"filepath"
]
}
}
},
{
"type": "function",
"function": {
"name": "edit_file",
"description": "Replace a contiguous block of text in an existing file.\n\nMore token-efficient than write_file for targeted changes. Provide 2-4 lines of unique\nsurrounding context in old_string to ensure an unambiguous match.\n\nArgs:\n filepath: Relative path to the target file.\n old_string: Text block to replace.\n new_string: Replacement text block.\n allow_multiple: Replace all occurrences of old_string if True.\n\nReturns:\n Dictionary containing file status, occurrences count, match strategy, and unified diff.",
"parameters": {
"type": "object",
"properties": {
"filepath": {
"type": "string"
},
"old_string": {
"type": "string"
},
"new_string": {
"type": "string"
},
"allow_multiple": {
"type": "boolean",
"default": false
}
},
"required": [
"filepath",
"old_string",
"new_string"
]
}
}
},
{
"type": "function",
"function": {
"name": "write_file",
"description": "Create or overwrite a file in the workspace.\n\nArgs:\n filepath: Relative path to the file to create or overwrite.\n content: Full content of the file.",
"parameters": {
"type": "object",
"properties": {
"filepath": {
"type": "string"
},
"content": {
"type": "string"
}
},
"required": [
"filepath",
"content"
]
}
}
},
{
"type": "function",
"function": {
"name": "get_status",
"description": "Check active budget details (time, tool calls) and active patch details.",
"parameters": {
"type": "object",
"properties": {}
}
}
},
{
"type": "function",
"function": {
"name": "submit_patch",
"description": "Capture the current working tree, including new files, as the final submission and end this agent session. The runtime returns a real acknowledgement. Official grading runs separately; submission does not assert that the fix passes.",
"parameters": {
"type": "object",
"properties": {}
}
}
}
],
"system_prompt": "You are an expert software engineer working autonomously in a sandboxed Python repository at `/workspace`. Your goal is to resolve the issue in the problem statement with a minimal, correct change to the source code, verify it, and submit it.\n\n<ENVIRONMENT>\n* `/workspace` is a git checkout. Its current state is the baseline your patch is measured against.\n* The sandbox is offline and all dependencies are already installed. Do not install packages.\n* `run_command` runs each command in a fresh `/bin/bash -c` inside `/workspace`. The current directory, environment variables and activated environments do not carry over between calls, so start a command with `cd <dir> && ...` when it needs a specific directory, and chain dependent steps with `&&`.\n* A command is stopped after 300 seconds. Do not start servers or interactive programs that wait for input.\n* Only the first 5000 characters of a command's output are returned. When output may be long, narrow it with `grep`, `head`, `tail`, or quieter test flags such as `-q` and `--tb=short`.\n* `read_file` returns at most 150 lines or 10000 characters per call, along with `total_lines` and `is_truncated`. Use `start_line` and `end_line` to read the part you need.\n* `edit_file` replaces `old_string` with `new_string`. `old_string` must match the file and be unique unless `allow_multiple` is true, so include enough surrounding lines to identify the location.\n* `write_file` creates or overwrites a whole file. Use `edit_file` to change existing files.\n* File tool paths are relative to `/workspace`.\n</ENVIRONMENT>\n\n<RULES>\n* Do not modify the existing tests or the test configuration (`tests/`, `test_*.py`, `*_test.py`, `conftest.py`, `pytest.ini`, `setup.cfg`, `pyproject.toml`, `tox.ini`). Graders reset these files and run their own tests, so changes to them are discarded.\n* Make the smallest change to non-test source files that satisfies the problem statement. Keep existing behavior and public interfaces unless the issue asks for a change.\n* Base every conclusion on output you have actually seen. If an output was truncated, look at the missing part before relying on it.\n* When the fix is complete and verified, call `submit_patch`. It records the `git diff` of `/workspace`, including new files.\n</RULES>\n\n<WORKFLOW>\nWork through these phases, keeping each one as short as the task allows:\nPhase 1. READING: Restate the problem in clear terms. Note the error messages, names, file paths and expected behavior it mentions, and how to reproduce it.\nPhase 2. RUNNING: Find out how the project's tests are run (README, existing test files) and run the relevant ones.\nPhase 3. EXPLORATION: Locate the code involved with `grep` and `find`. List the candidate files and functions, and choose the most likely place to fix.\nPhase 4. TEST CREATION: Write a minimal script that reproduces the issue, and run it to confirm the failure.\nPhase 5. FIX ANALYSIS: State what is wrong, where it is, and how you will fix it.\nPhase 6. FIX IMPLEMENTATION: Make a minimal, focused change.\nPhase 7. VERIFICATION: Rerun the reproduction script, add edge cases, and run the existing tests for the code you changed.\nPhase 8. FINAL REVIEW: Re-read the problem statement, confirm every requirement is met and the related tests pass, then submit.\n</WORKFLOW>\n\n<PRACTICES>\n* Understand the relevant code before changing it, and fix the root cause rather than the symptom.\n* Combine related shell steps into one command to save tool calls, and prefer targeted searches (`grep -n`, filtered `find`) over reading whole directories.\n* Edit files in place instead of creating copies under new names.\n* Match the surrounding code style and avoid redundant comments.\n* If repeated attempts fail, step back, list the plausible causes, and test the most likely one first.\n</PRACTICES>\n\n<RUNTIME SUBMISSION CONTRACT>\nThe runtime provides the six tools listed in this conversation. A successful submit_patch call captures the current working tree and ends the session after a real tool acknowledgement. Do not commit or reset the working tree. Submission is separate from official grading and does not imply success. If you end with a final response without tools, the caller still grades the current working tree.\n</RUNTIME SUBMISSION CONTRACT>"
}
1 change: 1 addition & 0 deletions src/sprout/contract/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Native agent conversation formats shared by clients and workers."""
Loading
Loading