Skip to content

feat(opencode): support multiple activations per session - #4747

Merged
yaoyu-33 merged 10 commits into
mainfrom
flafrance/opencode-multiple-activations
Oct 21, 2026
Merged

yaoyu-33 merged 10 commits into
mainfrom
flafrance/opencode-multiple-activations

Conversation

@lafrancef

@lafrancef lafrancef commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Add multi-turn OpenCode continuation for SWE-Together's interactive coding episodes. Seed with continuation: {mode: resume_harness_session}, then append user input through ordinary /v1/responses requests using the session cookie and existing 1-based x-ng-agent-activation header. Use metadata.agent_timeout_sec for each activation's budget. The adapter retains its saved session and HOME between calls.

Each activation has its own invocation ID and status, with harness_session_id recorded as context. Trajectory turns carry the activation ID, and the plugin forwards activation-scoped X-Session-Id values for root and child invocations. Responses expose new output, usage and observations, with harness_session_id also in metadata; close returns the accumulated evidence. Requested in-harness tool observations use harness_tool_calls.

The shared ledger joins identical retries, rejects conflicting or out-of-order inputs, and fences execution during close. A final negative termination receipt fails the activation and skips verification; close releases the borrowed connection and returns cleanup_confirmed: false. Connection release failures remain retryable. Validation runs before consuming an index. Deadlines cover preparation, queueing and staging; expiry before the first launch allows a later activation to start. Retry guarantees remain process-local.

Continuation timeouts fail on explicit OpenCode error events, including after partial output. When close recovers delayed artifacts, it replaces that activation's fallback evidence while preserving its original outcome and earlier activations.

Existing single-turn response metadata, model headers and close behavior are preserved. Unknown cleanup confirmation is omitted. SandboxSession keeps finalization private and supports explicit rearming after confirmed cleanup with fresh launch files. The adapter README and integration guides document the protocol and directory ownership.

Live validation used OpenCode 1.17.11 and gpt-4.1-mini on borrowed OpenSandbox CPU sandboxes. Single-turn and two-activation file-repair smokes passed their external verifiers, identical replay checks, existing observability conformance checks and rollout health checks. The second activation resumed the same OpenCode session and recalled a label from the first. Agent cleanup disconnected the borrowed connection, leaving task files available for verification; the sandbox owner then destroyed the allocations.

No separate tracking issue is needed: this focused change supplies multi-turn continuation for the SWE-Together harness integration.

The terminology-only follow-up renames the protocol, OpenCode helpers, docs and tests consistently. Its focused checks passed (155 tests):

python -m pytest -q -o addopts= \
  responses_api_agents/opencode_agent/tests/test_continuation.py \
  responses_api_agents/opencode_agent/tests/test_opencode_headers.py \
  responses_api_agents/opencode_agent/tests/test_trajectory.py \
  tests/unit_tests/test_agent_activations.py \
  tests/unit_tests/test_rollout_observability.py
pre-commit run --all-files

Whole-tree pre-commit passed; its guardwords check skipped because the private catalog was unavailable. Full local CI and live smokes were not repeated for this renaming; GitHub CI is requested for the final commit.

Validation before the terminology-only follow-up (02aeba2b):

python -m pytest -q -o addopts= --import-mode=importlib \
  responses_api_agents/opencode_agent/tests \
  tests/unit_tests/test_agent_activations.py \
  tests/unit_tests/test_sandbox_session.py \
  tests/unit_tests/test_agent_sessions.py \
  tests/unit_tests/test_checkpoint_agent.py \
  tests/unit_tests/test_agent_session_retries.py \
  tests/unit_tests/test_base_responses_api_agent.py \
  tests/unit_tests/harness_capabilities \
  tests/unit_tests/test_rollout_observability.py \
  responses_api_agents/simple_agent/tests/test_app.py \
  scripts/harness_conformance/tests
bash scripts/ci/core_unit_tests.sh
source scripts/ci/setup_dev.sh
uv sync --extra dev --extra ray --extra sandbox --extra openshell --extra mlflow --extra wandb
pytest --cov=. --cov-append --cov-report=term-missing \
  --cov-fail-under="$(python3 scripts/ci/cov_fail_under.py)" --durations=10 -m sandbox
for shard in 0 1 2 3; do bash scripts/ci/server_tests.sh "$shard" 4; done
pre-commit run --all-files

The focused regression/checker selection passed: 993 tests, 42 platform skips. Whole-tree pre-commit passed. Fresh single-turn and two-activation live smokes passed verification, replay, observability conformance, rollout health and cleanup.

Linux core passed 8877 tests (28 skips), followed by 921 sandbox tests (5 skips). Combined coverage passed the configured 95% gate at 95.09%.

The full server run selected all 233 servers: 210 passed; 23 were blocked by GitHub HTTP 503 responses or PyTorch download connection resets. Both changed servers passed their full isolated Linux suites: OpenCode 472 passed/6 skips; NOOA 226 passed. There were no data-validation failures. Scripted OpenCode conformance statuses match the previous revision, including its existing sandbox.identity and model_error failures.

@copy-pr-bot

copy-pr-bot Bot commented Oct 19, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@lafrancef lafrancef added area:agent Agent harnesses and Responses API agent behavior feature New capabilities, enhancements, or enablement work labels Oct 19, 2026
@lafrancef
lafrancef marked this pull request as ready for review October 19, 2026 21:04

@yaoyu-33 yaoyu-33 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, @lafrancef. The ledger and per-activation rearm design is clear, and the README and fern updates make the protocol easy to follow. We ran the PR's tests locally (test_agent_activations: 12 passed; opencode_agent tests: 405 passed, 42 skipped on macOS) and reproduced the inline items with scratch probes against both base and head.

The main item is in opencode_agent/sandbox.py: the single-turn path's failure-time evidence changed, which the description says is preserved. The rest are smaller edges in the new continuation path, test coverage for the shared SandboxSession changes, and docs and contract clarity.

PR hygiene:

  • Could you link the tracking issue, or say why none is needed (AGENTS.md "Pull Requests")? Naming the intended consumer of the continuation fields would also help, since nothing in the repo sends continuation yet.
  • Could you list the exact local checks you ran (pytest targets, pre-commit run --all-files) next to the live-smoke description?
  • CI still needs a vetter's /ok to test.

Comment thread nemo_gym/base_responses_api_agent.py Resolved
Comment thread nemo_gym/base_responses_api_agent.py Resolved
Comment thread nemo_gym/base_responses_api_agent.py Resolved
@yaoyu-33 yaoyu-33 added complexity:high Cross-component or shared-contract change with a large review surface needs-review PR is ready for code review and waiting on a reviewer labels Oct 19, 2026
@yaoyu-33

Copy link
Copy Markdown
Contributor

Not blocking for this PR, but a suggestion to make the next harness cheaper to add (codex, hermes, openclaw and pi all use SandboxSession too).

Only three things look OpenCode-specific: the resume flag, where native state lives, and how to find new messages in the export. The rest is generic but lives in OpenCode behind ~20 is_continuation branches:

  • per-activation reset;
  • evidence accumulation;
  • activation-scoped IDs;
  • deadline handling;
  • the close bundle.

Could we split session state from per-activation state and move the generic part into nemo_gym/agent_utils?

@dataclass
class Activation:          # replaced each activation, no field-by-field reset
    id: int
    log: str = ""
    observations: AgentObservationBundle | None = None
    trajectory: TrajectoryRecord | None = None
    runtime_info: HarnessProcessInfo | None = None

@dataclass
class ContinuationState:   # retained across activations
    home_dir: str
    native_id: str | None = None
    seen_message_ids: set[str] = field(default_factory=set)
    evidence: EvidenceAccumulator = field(default_factory=EvidenceAccumulator)  # merge + ID scoping

Two related ideas:

  • One path for single-turn and continuation. Treat single-turn as a single activation on the same path, so most is_continuation branches go away. The single-turn evidence-ordering change came from one of them.
  • Fewer adapter hooks. Let the base route opted-in sessions to activate_agent_session(), and give _validate_agent_activation / _continuation_close_timeout defaults. Then an adapter mostly implements one method: run one activation with resume.

A follow-up PR is fine too.

@lafrancef
lafrancef requested a review from a team as a code owner October 20, 2026 09:26
@lafrancef

Copy link
Copy Markdown
Contributor Author

Ack, agreed splitting off the generic part makes sense, will do once we add multiple-activation support to a second harness

@lafrancef

Copy link
Copy Markdown
Contributor Author

/ok to test 9d4caf5

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

@lafrancef

Copy link
Copy Markdown
Contributor Author

/ok to test 2d711fd

Comment thread nemo_gym/agent_utils/sandbox_session.py Resolved
@lafrancef

Copy link
Copy Markdown
Contributor Author

/ok to test 221c064

… root

Signed-off-by: Frederic Lafrance <flafrance@nvidia.com>
@lafrancef

Copy link
Copy Markdown
Contributor Author

/ok to test ea17c8b

yaoyu-33
yaoyu-33 previously approved these changes Oct 21, 2026
@yaoyu-33
yaoyu-33 enabled auto-merge October 21, 2026 17:35
…bump

Signed-off-by: Frederic Lafrance <flafrance@nvidia.com>
@lafrancef

Copy link
Copy Markdown
Contributor Author

/ok to test b3cc21c

@nemo-automation-bot
nemo-automation-bot Bot requested a review from a team October 21, 2026 17:45
@github-actions

Copy link
Copy Markdown
Contributor

🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/39649856807

Merged via the queue into main with commit 92323e9 Oct 21, 2026
50 of 56 checks passed
@yaoyu-33
yaoyu-33 deleted the flafrance/opencode-multiple-activations branch October 21, 2026 18:29
gwarmstrong added a commit to gwarmstrong/Gym that referenced this pull request Oct 21, 2026
NVIDIA-NeMo#4747's multi-activation tests expected process_supervisor.py, while this
branch uploads the supervisor without the suffix. Both now use
SUPERVISOR_FILE, so the OpenCode upload-delay test injects its delay again.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
yaoyu-33 pushed a commit to yaoyu-33/Gym that referenced this pull request Oct 26, 2026
NVIDIA-NeMo#4805)

* fix(agents): hide the sandbox supervisor from pkill python

Models clean up their own work with `pkill -f python3`, `pkill python3`,
`killall python3` and similar. In the task sandbox those matched Gym's
supervisor (`python3 -I .../process_supervisor.py`) and the harness runner
under it, so the supervisor stopped the agent CLI and the episode ended as
"Agent response status: failed" with reward 0. On long
Terminal-Bench-Science runs this ended Codex trials partway through, while
under a harness whose agent CLI is node or a native binary the same
commands are harmless.

The launch shell now links `<session>/bin/gym-supervisor` to the binary
behind the adapter's interpreter (`/proc/self/exe`, past venv links and
shims) and execs the supervisor through it, from `process_supervisor`
without a `.py` suffix. A runner that opts in with
`SandboxCommand.hide_runner` runs the same way as `bin/gym-runner`. Codex
and Pi opt in and upload their runner as `sandbox_runner`. Neither process
name nor command line mentions python, so none of
`pkill -f python3|python|"python3 -"|"\.py"`, `pkill python3|python` or
`killall python3` matches. Hermes keeps its venv interpreter for its runner
(the runner is the agent); its supervisor is hidden. OpenCode and OpenClaw
get the hidden supervisor too; their test fakes now match the new file
name. If the link cannot be made or cannot load the supervisor, both
processes keep the given interpreter. Gym's stop path signals the recorded
PID and is unchanged.

Tests: the shared supervisor tests add a pgrep-based identity test that
runs the patterns as pkill/killall would match them, a link fallback case
and a check that a venv runner is left alone. Codex, Pi, Hermes, OpenCode
and OpenClaw tests follow the new file names.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>

* fix(agents): hide the OpenCode and OpenClaw runners from pkill python

The OpenCode and OpenClaw adapters launch the same shared supervisor and a
standard-library runner (`python3 -I <session>/sandbox_runner.py`) above
their CLI, so a task agent's `pkill -f python3` or `killall python3` stops
the runner and ends the episode just as it did for Codex and Pi. The
supervisor is already hidden; opt the runners in too: upload them as
`sandbox_runner` and set `SandboxCommand.hide_runner`, so they run as
`bin/gym-runner`. OpenCode's post-cleanup transcript snapshot uses the
renamed file.

Tests: each adapter's exec-transport suite gains a test that launches the
real supervisor and runner and checks that neither process name nor
command line mentions python; the session fakes expect the new runner
name.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>

* fix(agents): don't recreate a retired session directory from a late launch

A launch delayed before it links the interpreter used mkdir -p for the
session's bin directory, so it recreated the session directory after close
had removed it. Create only bin, so the link fails once the session is gone.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>

* docs(agents): scope the pkill claim to runners that opt in

A harness argv that keeps its own python, such as Hermes's or Stirrup's,
still appears in the supervisor's command line, so pkill -f python can
match the supervisor there.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>

* test(agents): run the launch quoting test off Linux again

Only the interpreter links depend on /proc. Elsewhere the launch falls back
to the given interpreter, so expect that instead of skipping the test.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>

* refactor(agents): rename hide_runner to stdlib_runner and reject an argv it can't hide

The opt-in is a claim about the runner: it must need nothing beyond the
standard library, because the link runs the base interpreter outside any
virtual environment. Name it after that contract. Setting it for an argv
that doesn't start with the adapter's python used to do nothing silently;
SandboxCommand and supervised_launch_command now raise ValueError.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>

* test(kilocode): match the supervisor uploaded without a .py suffix

Kilo landed on main after this branch renamed the uploaded supervisor to
process_supervisor; its fake sandboxes still looked for process_supervisor.py.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>

* docs(agents): describe the supervisor links and stdlib_runner in the harness guide

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>

* test(agents): match the uploaded supervisor name in the activation tests

NVIDIA-NeMo#4747's multi-activation tests expected process_supervisor.py, while this
branch uploads the supervisor without the suffix. Both now use
SUPERVISOR_FILE, so the OpenCode upload-delay test injects its delay again.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>

* test(nooa): match the supervisor upload without the .py suffix

NOOA's sandbox runner landed on main after this branch renamed the uploaded supervisor to process_supervisor, so its tests still looked for process_supervisor.py and never routed the launch.

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>

---------

Signed-off-by: gwarmstrong <gwarmstrong@users.noreply.github.com>
Co-authored-by: gwarmstrong <gwarmstrong@users.noreply.github.com>

This branch was previously deployed

1 inactive deployment
public — b3cc21c4 Deployed Oct 21, 2026 by copy-pr-bot[bot] via release / finalize / notify #6326
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agent Agent harnesses and Responses API agent behavior complexity:high Cross-component or shared-contract change with a large review surface feature New capabilities, enhancements, or enablement work needs-review PR is ready for code review and waiting on a reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants