diff --git a/agentlightning/controller/local_reconciler.py b/agentlightning/controller/local_reconciler.py index 7971c829a..847bfe736 100644 --- a/agentlightning/controller/local_reconciler.py +++ b/agentlightning/controller/local_reconciler.py @@ -28,6 +28,10 @@ _SHUTDOWN_WAIT_TIMEOUT = 5.0 +def _is_native_windows() -> bool: + return os.name == "nt" + + def _run_local_reconciler_worker(agent_class_path: str) -> int: try: if ":" in agent_class_path: @@ -105,6 +109,10 @@ def __init__( self._stop = asyncio.Event() async def run(self) -> None: + if _is_native_windows(): + raise RuntimeError( + "runner_type=local is not supported on native Windows; use Linux (for example, WSL) instead." + ) log.info("LocalReconciler starting", pool_size=self._pool_size, tick=self._tick_interval) try: await self._reconcile_loop() diff --git a/docs/01-quick-start.md b/docs/01-quick-start.md index 95d094aa6..05ade8c4e 100644 --- a/docs/01-quick-start.md +++ b/docs/01-quick-start.md @@ -7,6 +7,8 @@ This quick start requires only one machine with one A100 GPU. It runs Agent Ligh Complete [Installation](00-installation.md), including the `verl` GPU stack. > AGL v1.0 itself is lightweight, but policy inference and GRPO updates still require the GPU stack used by `verl` and vLLM. +> +> This quick start uses `runner_type=local`, which is not supported on native Windows. On Windows, run it from a Linux environment such as WSL. See [Local runner limits](30-controller-configuration.md#local-runner-limits) for details. ## 1. Prepare the example diff --git a/docs/30-controller-configuration.md b/docs/30-controller-configuration.md index 2af5f2093..6ed8de93a 100644 --- a/docs/30-controller-configuration.md +++ b/docs/30-controller-configuration.md @@ -86,6 +86,8 @@ The local runner limits concurrent processes and periodically synchronizes their When the process pool reaches `maximum_size`, queued rollouts wait until capacity becomes available. +**Platform support:** The local runner requires POSIX process-group signals, so `runner_type=local` is not supported on native Windows. The Controller exits at startup, before it queries or dispatches any rollouts. Run the local controller from Linux (for example, WSL) instead; `runner_type=k8s` is unaffected. + ## How agents are launched In `k8s` mode, the Controller reads the Jinja Job template stored in each rollout. The template originates from `agentlightning.k8s.job_template_path` in the Trainer configuration. The Controller renders the template with the rollout's `input`, applies the Controller settings such as namespace, timeout, and cleanup TTL, and submits the resulting Kubernetes Job. It also injects rollout-specific `AGL_OPENAI_BASE_URL`, `AGL_EVENT_URL`, and `AGL_KEY` values into every container. See [Trainer Configuration](20-trainer-configuration.md#rollout-execution) for the template configuration and Jinja examples, and `agentlightning/controller/k8s_reconciler.py` for the implementation. diff --git a/tests/controller/test_local_reconciler_platform.py b/tests/controller/test_local_reconciler_platform.py new file mode 100644 index 000000000..18631982c --- /dev/null +++ b/tests/controller/test_local_reconciler_platform.py @@ -0,0 +1,61 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""Platform support tests for the local reconciler.""" + +from unittest.mock import AsyncMock + +import pytest +from omegaconf import OmegaConf + +from agentlightning.client import AgentLightningAsyncClient +from agentlightning.controller import local_reconciler +from agentlightning.controller.local_reconciler import LocalReconciler + + +@pytest.mark.asyncio +async def test_run_fails_before_reconciling_or_spawning_on_native_windows( + monkeypatch: pytest.MonkeyPatch, +) -> None: + api = AsyncMock(spec=AgentLightningAsyncClient) + config = OmegaConf.create( + { + "runner_type": "local", + "local_runner": {"maximum_size": 1, "poll_interval": 0.01}, + } + ) + reconciler = LocalReconciler(api, config) + reconcile_loop = AsyncMock() + shutdown = AsyncMock() + monkeypatch.setattr(reconciler, "_reconcile_loop", reconcile_loop) + monkeypatch.setattr(reconciler, "_shutdown", shutdown) + monkeypatch.setattr(local_reconciler, "_is_native_windows", lambda: True) + + with pytest.raises(RuntimeError, match="runner_type=local is not supported on native Windows"): + await reconciler.run() + + reconcile_loop.assert_not_awaited() + shutdown.assert_not_awaited() + api.get.assert_not_awaited() + api.patch.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_run_reconciles_and_shuts_down_on_supported_platform(monkeypatch: pytest.MonkeyPatch) -> None: + api = AsyncMock(spec=AgentLightningAsyncClient) + config = OmegaConf.create( + { + "runner_type": "local", + "local_runner": {"maximum_size": 1, "poll_interval": 0.01}, + } + ) + reconciler = LocalReconciler(api, config) + reconcile_loop = AsyncMock() + shutdown = AsyncMock() + monkeypatch.setattr(reconciler, "_reconcile_loop", reconcile_loop) + monkeypatch.setattr(reconciler, "_shutdown", shutdown) + monkeypatch.setattr(local_reconciler, "_is_native_windows", lambda: False) + + await reconciler.run() + + reconcile_loop.assert_awaited_once_with() + shutdown.assert_awaited_once_with()