Skip to content
Merged
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
8 changes: 8 additions & 0 deletions agentlightning/controller/local_reconciler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions docs/01-quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions docs/30-controller-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
61 changes: 61 additions & 0 deletions tests/controller/test_local_reconciler_platform.py
Original file line number Diff line number Diff line change
@@ -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()