Skip to content

Agent functions: a worker mode that runs the subproc path on another host - #59

Open
celve wants to merge 1 commit into
LIN-2516/discard-infra-abortsfrom
LIN-2710/agent-worker-mode
Open

celve wants to merge 1 commit into
LIN-2516/discard-infra-abortsfrom
LIN-2710/agent-worker-mode

Conversation

@celve

@celve celve commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Layer P22 of the CUA upstreaming stack. It is built and reviewed in celve, and goes upstream with the whole stack once every layer is finished. Base: P9's branch LIN-2516/discard-infra-aborts (#34), because a lost or unreachable worker reports InfraAbort. P23 (sandbox mode) stacks on this layer. Issue: LIN-2710, hub LIN-2504.

Problem. Every agent call runs on the rollout host: subproc gives each one a child process there. A host-process harness (Harbor computer-1, terminus-2) and its verifier burn that host's CPUs, about 3.2 cores of a GPU box in LIN-2399's Harbor recipe. Miles has no way to run them elsewhere.

Fix.

  • --custom-agent-function-mode worker. Each call goes to an agent worker (python -m miles.rollout.agentic.worker, aiohttp) at --custom-agent-function-worker-url, which runs it through the existing subproc path on its own host. Harbor is imported in-process on the worker, as under subproc, and Harbor itself is unchanged.
  • Protocol (miles/rollout/agentic/remote.py). Every worker response comes within 25 s, so a proxy that cuts slow responses can sit in between.
    • POST /calls takes {call_id, fn "module:qualname", JSON kwargs, env, scope} and is idempotent per call_id.
    • GET /calls/{id}?after&wait≤25 returns the child's report_to_caller items, then the outcome: an ok value, an InfraAbort with its exit_status, or an error with its traceback.
    • DELETE /calls/{id} cancels the call; its child gets SIGTERM, as a cancelled subproc call's does.
  • Worker.
    • It runs each call with the caller's forwarded env and the caller's rollout-limits scope plus a per-process id, so callers on different hosts never share a lock. The caller's env goes last into the child's env, so that scope wins over the worker's own.
    • --keep-env keeps the host's own value for named vars, such as paths that differ there.
    • A cancel that overtakes its submit leaves a tombstone, so the late submit starts nothing.
    • A call nobody polls for --orphan-timeout (120 s) is cancelled.
  • Caller.
    • --custom-agent-function-worker-env names the env vars sent with each call: by default HARBOR_*, AGENT_*, LITE_* and each provider's key-file and address vars from PROVIDER_CREDENTIALS. Names ending in SECRET, SECRET_KEY, TOKEN, PASSWORD or API_KEY, and every provider key_env_vars name, are never sent by value; a *_FILE path is.
    • A transport failure or 4xx/5xx is InfraAbort("WorkerUnreachable"), and a call the worker no longer has is InfraAbort("WorkerLost"). P9 discards both as infrastructure failures.
    • Worker mode without a URL fails generate before a session opens, not as a per-sample agent error.
  • Rollout abort. call_agent_abort_hook, which the oversampling abort and the hot-restart takeover both call, first cancels this process's worker calls. Every case ends the trial: an admitted call is cancelled by its DELETE; a submit the DELETE overtakes is refused by the tombstone; a call whose DELETE is lost stops being polled, so the worker's orphan timer ends it.
  • The abort is scoped to its rollout. Each call carries its router (sglang_router_ip:port) as abort_scope, and the hook cancels only that scope's calls. The dedicated eval engines (--eval-num-gpus) generate from the same process through their own router, so training's oversampling abort no longer cancels eval's calls. The hot-restart takeover passes every_scope=True. Without this, a Harbor smoke lost 14–21 of 64 eval episodes as WorkerCancelled. The hook reads the router only when worker calls are in flight, so subproc and inline runs are unchanged.
  • Light imports. TrainStepOutcome moves to miles/backends/training_utils/outcome.py (re-exported from types.py), and InfraAbort and the traceback wrapper move to miles/rollout/agentic/errors.py (re-exported from agent_function.py). The worker then imports without torch, sglang or ray, and remote.py imports them without a cycle.
  • Harbor example. --agent-worker-url on examples/experimental/harbor/run.py passes the worker flags. harbor_env_vars(..., agent_worker=True) calls provision_provider(remote=True), which skips the local key supply and SDK check but still forwards the provider's endpoint vars. The README gains a section on running the trials on a CPU box.
  • No new classes. The source PRs' per-call records and config dataclasses (_Inflight, AgentWorkerEndpoint, _Call, WorkerConfig) are module dicts, plain dicts and keyword arguments here.

Tests (EC2 CPU VM, py3.12, the CI CPU requirements, SGLang sglang-miles b0ed761650):

  • New tests/fast/rollout/agentic/test_worker.py (22 tests, ~19 s) runs a real worker on a local port: child process, env forwarding and --keep-env, scope, ordered reports, InfraAbort and error passthrough, cancel, rollout abort through the hook and through inference_rollout_train.abort, abort scoping, the tombstone, an abort during a held submit, a lost DELETE reaped by the orphan timer, secrets, resubmit, lost and unreachable workers, and the light import.
  • Fail without the fix, pass with it:
    • Abort scoping. With _reaches forced to True, test_a_rollout_abort_leaves_another_rollouts_calls_running fails on assert still_running.
    • Abort cancels worker calls. With the hook's cancel removed, test_a_rollout_abort_cancels_the_calls_running_on_workers, test_a_rollout_abort_leaves_another_rollouts_calls_running and test_the_rollout_abort_entrypoint_cancels_a_trial_waiting_on_its_environment fail with TimeoutError.
  • test_agentic_v2.py gains two tests: worker mode passes the URL, the forwarded env and the router scope to the call, and worker mode without a URL raises before a session opens. test_launch_common.py gains two for the launcher. test_hot_restart.py's fake hooks take the new keyword.
  • pytest -n 6 tests/fast/rollout/agentic tests/fast/examples/experimental/harbor tests/fast/utils/test_hot_restart.py tests/fast/utils/test_agent_abort_hook.py tests/fast/utils/test_typed_agent_abort.py tests/fast/utils/test_misc.py tests/fast/utils/event_logger tests/fast/rollout/generate_hub tests/fast/rollout/inference_rollout tests/fast/rollout/test_sglang_rollout.py tests/fast/test_train.py tests/fast/test_train_async.py: head 594 passed, 17 skipped, 6 failed; base 569 passed, 17 skipped, 5 failed. The 5 shared failures are generate_hub/test_multi_turn.py's agentic_tool_call cases, the same on the base. The sixth, inference_rollout/integration/test_multi_turn.py::test_rollout[eval-agentic_tool_call], lost a port race under -n 6 (address already in use) and passes serially at head.
  • pre-commit run on the changed files: passed.

Limits.

  • No auth. The worker has none and report items are pickles, so keep it on a private network.
  • Forwarded paths must exist on the worker unless --keep-env names them.

Related upstream work. None found for running agent functions on another host. This route supersedes the agent-server drafts #30 and celve/harbor#1.

Origin: #40 (merge 2782de698d), plus the abort_scope part of #43 (merge 6e77a81cb3), both on LIN-1985/investigate-existing-recipes-for (LIN-2510). #43's recipe defaults, per-router recorder and legacy-eval refusal are left out, and so are #40's Qwen3-VL CUA launcher changes, since that launcher is not upstream. Live 8×8 of LIN-2399's Harbor recipe at #40's head, worker on an EC2 m7i.8xlarge: turn 4.12 s → 4.09 s, GPU-box harness 3.17 → 0 cores, 17.5 CPU-s of worker per episode, 0 InfraAbort.

…host

--custom-agent-function-mode worker sends each agent call to an agent worker
(python -m miles.rollout.agentic.worker) at --custom-agent-function-worker-url,
which runs it through the existing subproc path there, so a harness's CPU work
leaves the GPU box. The protocol is a submit plus long-polls of at most 25 s
and a DELETE to cancel. The run's agent env vars travel with each call, never
credentials by value. A lost or unreachable worker is an InfraAbort, and a
rollout abort cancels only the calls of its own rollout (its router), so the
dedicated eval engines' calls survive training's oversampling abort.
TrainStepOutcome moves to a module of its own so the worker imports without
torch, sglang or ray.
Copilot AI balanced review requested due to automatic review settings November 5, 2026 07:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-05T07:33:27.439680Z a078777 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a078777516

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +82 to +83
except asyncio.CancelledError:
_finish(call, {"kind": "cancelled"})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drain teardown reports before marking cancelled calls done

When a rollout abort cancels a worker call, call_agent_function sends SIGTERM but leaves its shielded result reader running so the child's cancellation cleanup can still report. Marking the call done immediately here makes the remote caller stop polling before those reports arrive. For agents using report_sandbox(..., status="finished") during teardown, the final status is consequently lost and the dashboard continues showing an active sandbox after it has been destroyed. This breaks the behavior explicitly covered for subproc mode by test_a_hung_subproc_call_reports_before_it_returns_and_its_cancel_teardown. Keep a report-draining path alive through child cleanup, including when the caller task itself is cancelled.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants