Compare commits

...

10 Commits

Author SHA1 Message Date
agent-lightning-bot 289c9ca25b [Release] v0.2.2 (#298) (#301)
Deploy Documentation / deploy (push) Has been cancelled
CPU Test / Lint - fast (push) Has been cancelled
CPU Test / Lint - slow (push) Has been cancelled
CPU Test / Build documentation (push) Has been cancelled
CPU Test / Test with Python 3.10 (legacy) (push) Has been cancelled
CPU Test / Test with Python 3.11 (stable) (push) Has been cancelled
CPU Test / Test with Python 3.12 (stable) (push) Has been cancelled
CPU Test / Test with Python 3.13 (latest) (push) Has been cancelled
PyPI Release / check-version (push) Has been cancelled
PyPI Release / publish-pypi (push) Has been cancelled
(cherry picked from commit a0bc1827d9)

Co-authored-by: Yuge Zhang <Yuge.Zhang@microsoft.com>
2025-11-12 15:58:38 +00:00
agent-lightning-bot 3ed230f439 Fix local model support in VERL (#299) (#300)
(cherry picked from commit f2869cea30)

Co-authored-by: Yuge Zhang <Yuge.Zhang@microsoft.com>
2025-11-12 15:00:23 +00:00
Yuge Zhang 1d2515c04e Fix preparing status transition on rollout when creating attempts (#278) (#280)
* Fix preparing status transition on rollout when creating attempts (#278)

(cherry picked from commit e0b55ab057)
2025-11-05 23:52:13 +08:00
agent-lightning-bot 88e28be0bd Fix: Port conflict in tracer tests (#271) (#273)
(cherry picked from commit f717f9982f)

Co-authored-by: Yuge Zhang <Yuge.Zhang@microsoft.com>
2025-11-05 23:51:41 +08:00
agent-lightning-bot f8a3082646 fix openai_agent version for compatibility issue. (#265) (#268)
* fix openai_agent version for compatibility issue.

* gen uv.lock

---------


(cherry picked from commit 80531c9c28)

Co-authored-by: Ni Hao <nihao_athlon@126.com>
Co-authored-by: Hao Ni (CSI Interfusion Co Ltd) <v-nhao@microsoft.com>
2025-11-05 19:32:58 +08:00
agent-lightning-bot ef2e90ccaa Bump version to 0.2.2 (#248) (#252)
(cherry picked from commit 5c8ac61af6)

Co-authored-by: Yuge Zhang <Yuge.Zhang@microsoft.com>
2025-10-31 23:09:43 +08:00
agent-lightning-bot 2a5bdbf661 Add support for verl 0.6.0 (#246) (#251)
(cherry picked from commit a02e1b91d9)

Co-authored-by: Zhiyuan He <zhiyuhe@microsoft.com>
2025-10-31 18:14:11 +08:00
agent-lightning-bot 01101a64ba [Release] v0.2.1 (#243) (#244)
Deploy Documentation / deploy (push) Has been cancelled
PyPI Release / check-version (push) Has been cancelled
PyPI Release / publish-pypi (push) Has been cancelled
(cherry picked from commit 80d306ff54)

Co-authored-by: Yuge Zhang <Yuge.Zhang@microsoft.com>
2025-10-30 09:41:17 +08:00
agent-lightning-bot ef346fac0f propose fix a couple of typos and avoid emacs backup files (#237) (#238)
(cherry picked from commit f8c45b6ca8)

Signed-off-by: John E <jeis4wpi@outlook.com>
Co-authored-by: John Eismeier <42679190+jeis4wpi@users.noreply.github.com>
2025-10-29 10:34:43 +08:00
agent-lightning-bot 41c6ec2bd9 Fix store port conflict handling (issue #221) (#227) (#235)
(cherry picked from commit 01955aead7)

Co-authored-by: ddsfda99 <168000702+ddsfda99@users.noreply.github.com>
2025-10-28 15:19:33 +08:00
26 changed files with 586 additions and 103 deletions
+16
View File
@@ -137,6 +137,22 @@ jobs:
WANDB_BASE_URL: ${{ secrets.MSR_WANDB_BASE_URL }}
WANDB_API_KEY: ${{ secrets.MSR_WANDB_API_KEY }}
- name: Calc-X training with local model
run: |
set -ex
source .venv/bin/activate
cd examples/calc_x
../../scripts/restart_ray.sh
sleep 5
hf download Qwen/Qwen2.5-0.5B-Instruct --local-dir data/qwen_model
PYTHONUNBUFFERED=1 python train_calc_agent.py --val-file data/test_mini.parquet --ci --model $(realpath data/qwen_model)
sleep 10
shell: bash
env:
WANDB_BASE_URL: ${{ secrets.MSR_WANDB_BASE_URL }}
WANDB_API_KEY: ${{ secrets.MSR_WANDB_API_KEY }}
id: calc_x_train_local_model
- name: Calc-X training LLM Proxy
run: |
set -ex
+4
View File
@@ -54,6 +54,10 @@ jobs:
run: |
uv sync --frozen --no-default-groups --extra apo --extra verl \
--group dev --group experiment --group agents --group torch-gpu-${{ matrix.setup-script }}
- name: Override VERL (stable)
run: |
uv pip install verl==0.5.0
if: matrix.setup-script == 'stable'
- name: Freeze dependencies
run: |
set -ex
+3
View File
@@ -189,6 +189,9 @@ cython_debug/
# you could uncomment the following to ignore the enitre vscode folder
.vscode/
# Emacs backup files
*~
# Ruff stuff:
.ruff_cache/
+1 -1
View File
@@ -1,6 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.
__version__ = "0.2.1"
__version__ = "0.2.2"
from .adapter import *
from .algorithm import *
@@ -99,6 +99,8 @@ class VERL(Algorithm):
# Merge your dict overrides
override_conf = OmegaConf.create(config)
# Allow adding new fields
OmegaConf.set_struct(base_cfg, False)
self.config = OmegaConf.merge(base_cfg, override_conf)
def run(
+8 -1
View File
@@ -6,12 +6,15 @@ from __future__ import annotations
import argparse
import asyncio
import logging
from typing import Iterable
from agentlightning.logging import configure_logger
from agentlightning.store.client_server import LightningStoreServer
from agentlightning.store.memory import InMemoryLightningStore
logger = logging.getLogger(__name__)
def main(argv: Iterable[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Run a LightningStore server")
@@ -22,7 +25,11 @@ def main(argv: Iterable[str] | None = None) -> int:
store = InMemoryLightningStore()
server = LightningStoreServer(store, host="0.0.0.0", port=args.port)
asyncio.run(server.run_forever())
try:
asyncio.run(server.run_forever())
except RuntimeError as exc:
logger.error("LightningStore server failed to start: %s", exc, exc_info=True)
return 1
return 0
+11 -14
View File
@@ -28,6 +28,12 @@ _original_handle_chat_attributes: Callable[..., Any] | None = None
_original_handle_response: Callable[..., Any] | None = None
def _unwrap_legacy_response(response: Any) -> Any:
if hasattr(response, "parse") and callable(response.parse):
return response.parse()
return response
def _patch_new_agentops():
import agentops.instrumentation.providers.openai.stream_wrapper
import agentops.instrumentation.providers.openai.wrappers.chat
@@ -44,6 +50,11 @@ def _patch_new_agentops():
@no_type_check
def _handle_chat_attributes_with_tokens(args=None, kwargs=None, return_value=None, **kws): # type: ignore
attributes = _original_handle_chat_attributes(args=args, kwargs=kwargs, return_value=return_value, **kws)
# In some cases, response is a openai._legacy_response.LegacyAPIResponse (e.g., LiteLLM, or LangChain),
# This is created by client.with_raw_response.create()
return_value = _unwrap_legacy_response(return_value)
if (
return_value is not None
and hasattr(return_value, "prompt_token_ids")
@@ -89,20 +100,6 @@ def _patch_new_agentops():
[logprob.model_dump() for logprob in first_choice.logprobs.refusal]
)
# For LiteLLM, response is a openai._legacy_response.LegacyAPIResponse
if (
return_value is not None
and hasattr(return_value, "http_response")
and return_value.http_response is not None
and hasattr(return_value.http_response, "json")
):
json_data = return_value.http_response.json()
if isinstance(json_data, dict):
if json_data.get("prompt_token_ids") is not None:
attributes["prompt_token_ids"] = list(json_data["prompt_token_ids"])
if json_data.get("response_token_ids") is not None:
attributes["response_token_ids"] = list(json_data["response_token_ids"][0])
return attributes
agentops.instrumentation.providers.openai.wrappers.chat.handle_chat_attributes = _handle_chat_attributes_with_tokens
+76 -6
View File
@@ -109,6 +109,7 @@ class LightningStoreServer(LightningStore):
self._uvicorn_server: uvicorn.Server | None = uvicorn.Server(self._uvicorn_config)
self._serving_thread: Optional[threading.Thread] = None
self._server_start_exception: Optional[BaseException] = None
# Process-awareness:
# LightningStoreServer holds a plain Python object (self.store) in one process
@@ -167,17 +168,45 @@ class LightningStoreServer(LightningStore):
logger.info(f"Starting server at {self.endpoint}")
uvicorn_server = self._uvicorn_server
self._server_start_exception = None
def run_server_forever():
asyncio.run(uvicorn_server.serve())
try:
asyncio.run(uvicorn_server.serve())
except (SystemExit, Exception) as exc:
logger.debug("LightningStore server thread exiting due to %s", exc, exc_info=exc)
self._server_start_exception = exc
self._serving_thread = threading.Thread(target=run_server_forever, daemon=True)
self._serving_thread.start()
serving_thread = threading.Thread(target=run_server_forever, daemon=True)
self._serving_thread = serving_thread
serving_thread.start()
# Wait for /health to be available
if not await self._server_health_check():
# Wait for uvicorn to report that it has started before pinging /health.
start_deadline = time.time() + 10
while time.time() < start_deadline:
if uvicorn_server.started:
break
if self._server_start_exception is not None or not serving_thread.is_alive():
self._handle_failed_start()
raise RuntimeError(self._format_start_failure_reason())
await asyncio.sleep(0.05)
else:
self._handle_failed_start()
raise RuntimeError("Server failed to start within the 10 seconds.")
# Wait for /health to be available once uvicorn reports started.
if not await self._server_health_check():
self._handle_failed_start()
raise RuntimeError("Server failed to start within the 10 seconds.")
# If startup failed (e.g. port already in use), uvicorn never flips `started`
# and the worker thread stops immediately. Guard against latching on to a
# different process that happened to satisfy the health check.
if not uvicorn_server.started or not serving_thread.is_alive() or self._server_start_exception is not None:
self._handle_failed_start()
failure_reason = self._format_start_failure_reason()
raise RuntimeError(failure_reason)
async def _server_health_check(self) -> bool:
"""Checks if the server is healthy."""
current_time = time.time()
@@ -190,12 +219,32 @@ class LightningStoreServer(LightningStore):
await asyncio.sleep(0.1)
return False
def _handle_failed_start(self) -> None:
"""Clean up thread state when startup fails."""
if self._uvicorn_server is not None:
self._uvicorn_server.should_exit = True
if self._serving_thread is not None:
# Thread already exited in most failure scenarios; join defensively.
self._serving_thread.join(timeout=0.1)
self._serving_thread = None
def _format_start_failure_reason(self) -> str:
base_message = f"LightningStore server failed to start on {self.endpoint}."
if isinstance(self._server_start_exception, SystemExit):
return f"{base_message} Another process may already be using this port."
if isinstance(self._server_start_exception, OSError):
return f"{base_message} {self._server_start_exception.strerror}."
if self._server_start_exception is not None:
return f"{base_message} Reason: {self._server_start_exception}."
return f"{base_message} Another process may already be using this port."
async def run_forever(self):
"""Runs the FastAPI server indefinitely.
You need to call this method in the same process as the server was created in.
"""
assert self._uvicorn_server is not None
uvicorn_server = self._uvicorn_server
async def _wait_till_healthy():
health = await self._server_health_check()
@@ -203,9 +252,30 @@ class LightningStoreServer(LightningStore):
raise RuntimeError("Server did not become healthy within the 10 seconds.")
logger.info("Store server is online at %s", self.endpoint)
async def _serve_capture():
try:
await uvicorn_server.serve()
except KeyboardInterrupt:
raise
except (SystemExit, Exception) as exc:
logger.debug("LightningStore server serve() raised %s", exc, exc_info=exc)
self._server_start_exception = exc
raise RuntimeError("LightningStore server failed to serve") from exc
# We run _wait_till_healthy and self._uvicorn_server.serve in parallel
# until one of them raises an exception.
await asyncio.gather(_wait_till_healthy(), self._uvicorn_server.serve())
try:
await asyncio.gather(_wait_till_healthy(), _serve_capture())
except BaseException as exc:
if isinstance(exc, KeyboardInterrupt):
raise
startup_failed = not uvicorn_server.started or isinstance(
self._server_start_exception, (SystemExit, OSError)
)
if startup_failed:
self._handle_failed_start()
raise RuntimeError(self._format_start_failure_reason())
raise
async def stop(self):
"""Gracefully stops the running FastAPI server.
+6
View File
@@ -369,6 +369,9 @@ class InMemoryLightningStore(LightningStore):
self._attempts[rollout.rollout_id] = []
self._attempts[rollout.rollout_id].append(attempt)
# Sync attempt status to rollout
await self._update_rollout_unlocked(rollout.rollout_id, status="preparing")
return AttemptedRollout(**rollout.model_dump(), attempt=attempt)
# If not in queuing state, skip this rollout and continue
@@ -413,6 +416,9 @@ class InMemoryLightningStore(LightningStore):
self._attempts[rollout_id] = []
self._attempts[rollout_id].append(attempt)
# Sync attempt status to rollout
await self._update_rollout_unlocked(rollout_id, status="preparing")
self._completion_events.setdefault(rollout.rollout_id, threading.Event())
return AttemptedRollout(**rollout.model_dump(), attempt=attempt)
+9 -1
View File
@@ -2,10 +2,12 @@
# type: ignore
from importlib.metadata import version
from typing import Any
import hydra
import ray
from packaging import version as packaging_version
from verl.trainer.main_ppo import create_rl_sampler
from verl.trainer.ppo.reward import load_reward_manager
@@ -39,11 +41,17 @@ def run_ppo(
) -> None:
if not ray.is_initialized():
# this is for local ray cluster
try:
# verl >= 0.6.0
num_cpus = config.ray_kwargs.ray_init.num_cpus
except AttributeError:
# verl < 0.6.0
num_cpus = config.ray_init.num_cpus
ray.init(
runtime_env={
"env_vars": {"TOKENIZERS_PARALLELISM": "true", "NCCL_DEBUG": "WARN", "VLLM_LOGGING_LEVEL": "WARN"}
},
num_cpus=config.ray_init.num_cpus,
num_cpus=num_cpus,
)
runner = TaskRunner.remote()
+11 -4
View File
@@ -12,6 +12,7 @@ from typing import Dict, Tuple
import numpy as np
import torch
import verl
from codetiming import Timer
from omegaconf import OmegaConf
from tqdm import tqdm
@@ -298,14 +299,20 @@ class AgentLightningTrainer(RayPPOTrainer):
assert self.async_rollout_mode, "If agent mode is enabled, async server must be enabled"
if self.adapter is not None and not isinstance(self.adapter, TraceToTripletBase):
raise ValueError("Adapter must be a TraceToTripletBase for currently VERL implementation.")
verl_version = verl.__version__
if verl_version == "0.5.0":
# Note (Zhiyuan): To avoid further patch into vllm async server, using the same sentence to get the naming here.
# However, it is possible that verl updates the naming and causes incompatibility.
# Reference: https://github.com/volcengine/verl/blob/5b5e09d9cc20625e436d01f69d9cc739ff681c54/verl/workers/rollout/vllm_rollout/vllm_async_server.py#L217
model = "/".join(self.config.actor_rollout_ref.model.path.split("/")[-2:])
else:
# For other versions (e.g., 0.6.0), we use the full path to the model.
model = self.config.actor_rollout_ref.model.path
self.agent_mode_daemon = AgentModeDaemon(
self.config.agentlightning.port,
self.config.actor_rollout_ref.rollout.n,
train_information={
# Note (Zhiyuan): To avoid further patch into vllm async server, using the same sentence to get the naming here.
# However, it is possible that verl updates the naming and causes incompatibility.
# Reference: https://github.com/volcengine/verl/blob/5b5e09d9cc20625e436d01f69d9cc739ff681c54/verl/workers/rollout/vllm_rollout/vllm_async_server.py#L217
"model": "/".join(self.config.actor_rollout_ref.model.path.split("/")[-2:]),
"model": model,
"temperature": self.config.actor_rollout_ref.rollout.temperature,
},
tokenizer=self.tokenizer,
+1 -1
View File
@@ -32,7 +32,7 @@ pip install agentlightning[verl]
!!! note
The resource type created by [VERL][agentlightning.algorithm.verl.VERL] is actually a [ProxyLLM][agentlightning.ProxyLLM], a subclass of the [LLM][agentlightning.LLM] type. This object contains a **URL template** provided by [VERL][agentlightning.algorithm.verl.VERL], with placeholders for rollout and attempt IDs. When a rollout begins on the agent side, the framework uses the current `rollout_id` and `attempt_id` to format this template, generating a final, unique endpoint URL. This URL points to [VERL][agentlightning.algorithm.verl.VERL]'s internal proxy, allowing it to intercept and log all traffic for that specific attempt, for tracing and load balancing purposes. For agents created with the `@rollout` decorator, this resolution of the template is handled automatically ("auto-stripped"). Class-based agents will need to manually resolve the `ProxyLLM` using the rollout context.
The resource type created by [VERL][agentlightning.algorithm.verl.VERL] is actually a [ProxyLLM][agentlightning.ProxyLLM], a subclass of the [LLM][agentlightning.LLM] type. This object contains a **URL template** provided by [VERL][agentlightning.algorithm.verl.VERL], with placeholders for rollout and attempt IDs. When a rollout begins on the agent side, the framework uses the current `rollout_id` and `attempt_id` to format this template, generating a final, unique endpoint URL. This URL points to [VERL][agentlightning.algorithm.verl.VERL]'s internal proxy, allowing it to intercept and log all traffic for that specific attempt, for tracing and load balancing purposes. For agents created with the `@rollout` decorator, this resolution of the template is handled automatically ("auto-stripped"). Class-based agents will need to manually resolve the [`ProxyLLM`][agentlightning.ProxyLLM] using the rollout context.
```python
proxy_llm = resources["main_llm"]
+119
View File
@@ -0,0 +1,119 @@
# Changelog
## Agent-lightning v0.2.2 (11/12/2025)
Agent-lightning v0.2.2 is a stabilization release for v0.2.1. It introduces several bug fixes.
* Fix compatibility issues with VERL 0.6.0.
* Fix model name for pre-downloaded models in VERL.
* Fix preparing status transition on rollout when creating attempts.
* Fix OpenAI Agents SDK compatibility issues.
**Full Changelog**: https://github.com/microsoft/agent-lightning/compare/v0.2.1...v0.2.2
---
## Agent-lightning v0.2.1 (10/30/2025)
Agent-lightning v0.2.1 is a stabilization release for v0.2.0. It introduces several bug fixes and new features, plus a number of unlisted CI improvements.
### Bug fixes
* Fix LiteLLM issues when restarting the proxy multiple times in the same process (#174 #206)
* Fix LiteLLM model name selection when multiple servers use the same model (#197)
* Fix store port conflict handling (#227)
### New Features
* Add trainer port option for client-server strategies (#198)
### Documentation
* Add tutorial for launching workers on separate machines (#213)
* Add link to VERL framework (#210)
* Add link to vLLM blog (#215)
* Fix a couple of typos and avoid emacs backup files (#237)
### New Contributors
A warm welcome to our first-time contributors: @scott-vsi, @ddsfda99, @jeis4wpi 🎉
**Full Changelog**: https://github.com/microsoft/agent-lightning/compare/v0.2.0...v0.2.1
---
## Agent-lightning v0.2.0 (10/22/2025)
Agent-Lightning v0.2.0 introduces major framework improvements, new execution strategies, expanded documentation, and enhanced reliability across the agent training and deployment workflow. This release includes **78 pull requests** since v0.1.2.
### Core Enhancements
* **Lightning Store**: Added unified interface and implementation for Agent-lightning's core storage.
* **Emitter**: Emitting any objects as spans to the store.
* **Adapter** and **Tracer**: Adapting to OpenAI-like messages, and OpenTelemetry dummy tracer.
* **LLM Proxy**: Added LLM Proxy as the first-class citizen in Agent-lightning.
* **Agent Runner**: New version providing a more modular and robust runner design.
* **Embedded Algorithms**: Algorithms are now embedded directly into trainers for simplicity.
* **New Execution Strategies**: Introduced *Client-Server* and *Shared Memory* execution models.
* **Trainer Updates**: Integrated v0.2 interfaces and FastAlgorithm validation.
### Documentation & Examples
* Revamped documentation with new guides for **agent creation**, **training**, **debugging**, and **store concepts**.
* Improved quickstart tutorials, clarified installation and new deep-dive articles.
* Added and updated examples: *SQL Agent*, *Calc-X*, *Local SFT*, *Search-R1*, and *APO algorithm*.
### Developer Experience
* Migrated build and CI pipelines to **1ES**, split workflows and aggregate badges for clarity.
* Adopted **uv** as the dependency manager.
* Added GPU-based pytest workflows for full test coverage.
* Enhanced debugging UX, pre-commit configs, and linting (Pyright fixes, import sorting).
### Ecosystem & Integrations
* Added support for agents built with [**Agent-framework**](https://github.com/microsoft/agent-framework).
* Added new community listings: [*DeepWerewolf*](https://github.com/af-74413592/DeepWerewolf) and [*AgentFlow*](https://agentflow.stanford.edu/).
### New Contributors
A warm welcome to our first-time contributors:
@hzy46, @lunaqiu, @syeehyn, @linhx1999, @SiyunZhao, and @acured 🎉
**Full changelog:** [v0.1.2 → v0.2.0](https://github.com/microsoft/agent-lightning/compare/v0.1.2...v0.2.0)
---
## Agent-lightning v0.1.2 (08/12/2025)
### What's Changed
* Add basic documentation in https://github.com/microsoft/agent-lightning/pull/33
* RAG example by @wizardlancet in https://github.com/microsoft/agent-lightning/pull/21
### New Contributors
* @wizardlancet made their first contribution in https://github.com/microsoft/agent-lightning/pull/21
**Full Changelog**: https://github.com/microsoft/agent-lightning/compare/v0.1.1...v0.1.2
---
## Agent-lightning v0.1.1 (08/06/2025)
### What's Changed
* Disable HTTP tracer tests and bump to 0.1.1 in https://github.com/microsoft/agent-lightning/pull/26
* Fix trainer bugs in v0.1 in https://github.com/microsoft/agent-lightning/pull/24
**Full Changelog**: https://github.com/microsoft/agent-lightning/compare/v0.1...v0.1.1
---
## Agent-lightning v0.1.0 (08/04/2025)
The first release of Agent-lightning!
- Turn your agent into an optimizable beast with **ZERO CODE CHANGE** (almost)! 💤
- Build with **ANY** agent framework (LangChain, OpenAI Agent SDK, AutoGen, CrewAI, ...); or even WITHOUT agent framework (Python OpenAI). You name it! 🤖
- **Selectively** optimize one or more agents in a multi-agent system. 🎯
- Embraces Reinforcement Learning, Automatic Prompt Optimization and more **algorithms**. 🤗
Install via `pip install agentlightning`.
+1 -1
View File
@@ -496,7 +496,7 @@ flowchart TD
Continuous learning keeps the algorithm loop running while runners report tasks and spans opportunistically. Key differences from batch mode:
1. The algorithm does not enqueue rollouts from a fixed dataset. Runners report tasks/rollouts and spans spontaneously.
2. The algorithm can wait for rollouts with a expected set of rollout IDs, but more oftenly polls for new rollouts and spans or waits for a count to arrive.
2. The algorithm can wait for rollouts with a expected set of rollout IDs, but more often polls for new rollouts and spans or waits for a count to arrive.
3. The [`Runner`][agentlightning.Runner] processes one rollout at a time via [`step(task)`][agentlightning.Runner.step] instead of exhausting a task queue. It notifies the store when starting a rollout so the store records it.
4. A user or higher-level loop controls which resources the next step uses and when to retry.
+1 -1
View File
@@ -264,7 +264,7 @@ dev_data = pd.read_parquet("data/test_dev_500.parquet").to_dict("records")[:10]
trainer.dev(agent, dev_dataset=dev_data)
```
Run this in a Python session or adapt your script to include a `--dev` flag. Once the spans appear healthy and the rewards are non-zero, switch back to [`trainer.fit(...)`][agentlightning.Trainer.fit] for full RL training.
Run this in a Python session or adapt your script to include a `--dev` flag. Once the spans appear healthy and the rewards are non-zero, switch back to [`trainer.fit(...)`][agentlightning.Trainer.fit] for full RL training. See the [debugging tutorial](../tutorials/debug.md) for more tips on how to debug the agent.
## Running the Sample Code
+42
View File
@@ -66,6 +66,48 @@ Swap in an [`AgentOpsTracer`][agentlightning.AgentOpsTracer] instead of [`OtelTr
You can also call [`Runner.step`][agentlightning.Runner.step] to inject ad-hoc rollouts into a running store being used by another algorithm, so that the rollouts can be consumed by the algorithms. This is very recently known as the paradigm of ["online RL"](https://cursor.com/blog/tab-rl). At the moment, no algorithm in the [algorithm zoo](../algorithm-zoo/index.md) consumes externally generated rollouts, but the data flow is available there if you need it.
## Debug with LLM Proxy
If you are dealing with LLM optimization like Reinforcement Learning, we generally recommend using an online stable LLM service for your debugging purposes, like `openai/gpt-4.1-nano`. After the debugging is done, you can switch to a local training endpoint.
However, if you want to use a local LLM features like [getting the token IDs](../deep-dive/serving-llm.md), you can also manually start a local vLLM server by:
```bash
vllm serve Qwen/Qwen2.5-0.5B-Instruct --port 8080
```
Then start the LLM proxy via the following script:
```python
import agentlightning as agl
llm_proxy = agl.LLMProxy(
port=8081,
model_list=[
{
"model_name": "Qwen/Qwen2.5-0.5B-Instruct",
"litellm_params": {
"model": "hosted_vllm/Qwen/Qwen2.5-0.5B-Instruct",
"api_base": "http://localhost:8080/v1",
},
}
],
store=agl.InMemoryLightningStore(),
)
llm_proxy.start()
time.sleep(1000000)
```
You can now use the LLM proxy by specifying environment variables:
```bash
export OPENAI_API_BASE=http://localhost:8081/v1
export OPENAI_API_KEY=dummy
```
You might see warnings about `Missing or invalid rollout_id, attempt_id, or sequence_id` in the LLM proxy logs. This is fine because you don't have a rollout and attempt yet when you are debugging. When you started the training, the algorithm will create the rollouts for you and the warnings will go away.
## Hook into Runner's Lifecycle
[`Runner.run_context`][agentlightning.Runner.run_context] accepts a `hooks` argument so you can observe or augment lifecycle events without editing your agent. Hooks subclass [`Hook`][agentlightning.Hook] and can respond to four asynchronous callbacks: [`on_trace_start`][agentlightning.Hook.on_trace_start], [`on_rollout_start`][agentlightning.Hook.on_rollout_start], [`on_rollout_end`][agentlightning.Hook.on_rollout_end], and [`on_trace_end`][agentlightning.Hook.on_trace_end]. This is useful for:
+1 -1
View File
@@ -141,7 +141,7 @@ Return only a number between 0 and 1. No text, punctuation, or explanation."""
try:
content = result.choices[0].message.content
if content is None:
console.print(f"[bold blue][Judge][/bold blue] Judge retured no content: {result}")
console.print(f"[bold blue][Judge][/bold blue] Judge returned no content: {result}")
return 0.0
score = float(content)
console.print(f"[bold blue][Judge][/bold blue] Judge returned score: {score}")
+8 -1
View File
@@ -40,7 +40,7 @@ import agentlightning as agl
def verl_default_config() -> Dict[str, Any]:
return {
config = {
"algorithm": {
"adv_estimator": "grpo",
"use_kl_in_reward": False,
@@ -58,6 +58,12 @@ def verl_default_config() -> Dict[str, Any]:
"multi_turn": {"format": "hermes"},
"name": "vllm",
"gpu_memory_utilization": 0.6,
"engine_kwargs": {
"vllm": {
"enable_auto_tool_choice": True,
"tool_call_parser": "hermes",
}
},
},
"actor": {
"ppo_mini_batch_size": 32,
@@ -96,6 +102,7 @@ def verl_default_config() -> Dict[str, Any]:
"total_epochs": 2,
},
}
return config
def train(
+1 -1
View File
@@ -136,7 +136,7 @@ def replace_cur_year(query: str) -> str:
def get_cursor_from_path(sqlite_path: str):
try:
if not os.path.exists(sqlite_path):
print("Openning a new connection %s" % sqlite_path)
print("Opening a new connection %s" % sqlite_path)
connection = sqlite3.connect(sqlite_path)
except Exception as e:
print(sqlite_path)
+7
View File
@@ -52,6 +52,12 @@ RL_TRAINING_CONFIG: Dict[str, Any] = {
"multi_turn": {"format": "hermes"},
"name": "vllm",
"gpu_memory_utilization": 0.8,
"engine_kwargs": {
"vllm": {
"enable_auto_tool_choice": True,
"tool_call_parser": "hermes",
}
},
},
"actor": {
"ppo_mini_batch_size": 32,
@@ -139,6 +145,7 @@ def config_train_llama() -> Dict[str, Any]:
config = deepcopy(RL_TRAINING_CONFIG)
config["actor_rollout_ref"]["rollout"]["multi_turn"]["format"] = "llama3_json"
config["actor_rollout_ref"]["rollout"]["engine_kwargs"]["vllm"]["tool_call_parser"] = "llama3_json"
config["actor_rollout_ref"]["model"]["path"] = "meta-llama/Llama-3.2-1B-Instruct"
return config
+2
View File
@@ -123,6 +123,8 @@ nav:
- Trainer: reference/trainer.md
- Types: reference/types.md
- Internal: reference/internal.md
- Miscellaneous:
- Changelog: changelog.md
extra_css:
- https://unpkg.com/katex@0/dist/katex.min.css
+8 -3
View File
@@ -1,6 +1,6 @@
[project]
name = "agentlightning"
version = "0.2.1"
version = "0.2.2"
description = "Agent-lightning is the absolute trainer to light up AI agents."
readme = "README.md"
requires-python = ">=3.10"
@@ -30,8 +30,8 @@ apo = [
# It's not recommended to use agentlightning[verl] to install VERL and its dependencies.
# though it's listed here for completeness.
verl = [
"verl>=0.5.0,<0.6.0",
"vllm>=0.8.4,<0.11.0",
"verl>=0.5.0",
"vllm>=0.8.4,<0.11.0", # Due to interface change of ExternalZeroMQDistributedExecutor
]
[project.scripts]
@@ -112,12 +112,14 @@ torch-gpu-stable = [
{include-group = "torch-cu128"},
"flash-attn>=2.8.3",
"tensordict>=0.9.1",
"verl>=0.6.0",
]
# Use this instead of --group torch-legacy --group torch-gpu
torch-gpu-legacy = [
{include-group = "torch-legacy"},
{include-group = "torch-cu128"},
"flash-attn==2.8.1",
"verl==0.5.0",
]
# For the TRL/Unsloth example.
@@ -154,6 +156,9 @@ autogen = [
]
openai-agents = [
"openai-agents",
"openai<2.7.0",
# Compatibility issues with the latest version of OpenAI, temporarily fixed version
# Issue: https://github.com/openai/openai-agents-python/issues/2038
"mcp",
]
anthropic = [
+85 -41
View File
@@ -76,6 +76,40 @@ async def server_client() -> AsyncGenerator[Tuple[LightningStoreServer, Lightnin
await server.stop()
@pytest.mark.asyncio
async def test_server_start_rejects_port_conflict() -> None:
"""Ensure startup fails loudly when the port is already owned by another store."""
store_a = InMemoryLightningStore()
port = _get_free_port()
server_a = LightningStoreServer(store_a, "127.0.0.1", port)
await server_a.start()
store_b = InMemoryLightningStore()
server_b = LightningStoreServer(store_b, "127.0.0.1", port)
with pytest.raises(RuntimeError, match="Another process may already be using this port"):
await server_b.start()
await server_a.stop()
@pytest.mark.asyncio
async def test_run_forever_rejects_port_conflict() -> None:
"""Ensure run_forever also reports port conflicts with the friendly message."""
store_a = InMemoryLightningStore()
port = _get_free_port()
server_a = LightningStoreServer(store_a, "127.0.0.1", port)
await server_a.start()
store_b = InMemoryLightningStore()
server_b = LightningStoreServer(store_b, "127.0.0.1", port)
with pytest.raises(RuntimeError, match="Another process may already be using this port"):
await server_b.run_forever()
await server_a.stop()
@pytest.mark.asyncio
async def test_add_resources_via_server(server_client: Tuple[LightningStoreServer, LightningStoreClient]) -> None:
"""Test that add_resources works correctly via server."""
@@ -484,59 +518,69 @@ async def test_subprocess_client_operations_work_but_direct_store_access_fails()
@pytest.mark.asyncio
@pytest.mark.parametrize(
"status,endpoint,make_app_error",
[
(400, "/enqueue_rollout", True), # server-marked app error -> 400 -> no retry
(404, "/update_rollout", False), # non-408 4xx -> no retry
],
)
async def test_no_retry_on_4xx_application_and_non408(
@pytest.mark.flaky(reruns=3, reruns_delay=2)
async def test_retry_on_400_application_error(
server_client: Tuple[LightningStoreServer, LightningStoreClient],
monkeypatch: MonkeyPatch,
status: int,
endpoint: str,
make_app_error: bool,
) -> None:
"""Test that client retries on app-side 400 that becomes a 500 due to server exception handling."""
server, client = server_client
if make_app_error:
# Force app-side exception so server returns 400 via exception handler.
call_count = {"n": 0}
original = server.store.enqueue_rollout
# Force app-side exception so server returns 400 via exception handler.
call_count = {"n": 0}
original = server.store.enqueue_rollout
async def boom(*args: Any, **kwargs: Any) -> Any:
call_count["n"] += 1
raise RuntimeError("synthetic app error")
async def boom(*args: Any, **kwargs: Any) -> Any:
call_count["n"] += 1
raise RuntimeError("synthetic app error")
monkeypatch.setattr(server.store, "enqueue_rollout", boom, raising=True)
monkeypatch.setattr(server.store, "enqueue_rollout", boom, raising=True)
with pytest.raises(ClientResponseError) as ei:
await client.enqueue_rollout(input={"origin": "should-fail"})
assert ei.value.status == 400
assert call_count["n"] == 1
with pytest.raises(ClientResponseError) as ei:
await client.enqueue_rollout(input={"origin": "should-fail"})
monkeypatch.setattr(server.store, "enqueue_rollout", original, raising=True)
else:
# Raise 404 once for /update_rollout; client must not retry.
original_post = aiohttp.ClientSession.post
calls = {"n": 0}
assert ei.value.status == 400
assert call_count["n"] == 1
def post_404(self: aiohttp.ClientSession, url: Any, *args: Any, **kwargs: Any) -> MockResponse:
if str(url).endswith(endpoint):
calls["n"] += 1
req_info = aiohttp.RequestInfo(
url=URL(str(url)), method="POST", headers=cast(Any, {}), real_url=URL(str(url))
)
raise ClientResponseError(request_info=req_info, history=(), status=status, message="not found")
return MockResponse(original_post(self, url, *args, **kwargs))
# Restore original method
monkeypatch.setattr(server.store, "enqueue_rollout", original, raising=True)
monkeypatch.setattr(aiohttp.ClientSession, "post", post_404, raising=True)
with pytest.raises(ClientResponseError) as ei:
await client.update_rollout("nonexistent", status="running")
assert ei.value.status == 404
assert calls["n"] == 1
@pytest.mark.asyncio
async def test_no_retry_on_non408_4xx(
server_client: Tuple[LightningStoreServer, LightningStoreClient],
monkeypatch: MonkeyPatch,
) -> None:
"""Test that client does not retry on non-408 4xx errors such as 404."""
_, client = server_client
original_post = aiohttp.ClientSession.post
calls = {"n": 0}
def post_404(self: aiohttp.ClientSession, url: Any, *args: Any, **kwargs: Any):
if str(url).endswith("/update_rollout"):
calls["n"] += 1
req_info = aiohttp.RequestInfo(
url=URL(str(url)),
method="POST",
headers=cast(Any, {}),
real_url=URL(str(url)),
)
raise ClientResponseError(
request_info=req_info,
history=(),
status=404,
message="not found",
)
return MockResponse(original_post(self, url, *args, **kwargs))
monkeypatch.setattr(aiohttp.ClientSession, "post", post_404, raising=True)
with pytest.raises(ClientResponseError) as ei:
await client.update_rollout("nonexistent", status="running")
assert ei.value.status == 404
assert calls["n"] == 1
@pytest.mark.asyncio
+3 -3
View File
@@ -1639,7 +1639,7 @@ async def test_status_propagation_only_for_latest_attempt(inmemory_store: InMemo
# Rollout status should NOT change since attempt1 is not the latest
updated_rollout = await inmemory_store.get_rollout_by_id(rollout.rollout_id)
assert updated_rollout is not None
assert updated_rollout.status == "queuing" # Should remain unchanged
assert updated_rollout.status == "preparing" # Should be status of attempt 2
# Update attempt3 (latest) to succeeded
await inmemory_store.update_attempt(
@@ -1670,7 +1670,7 @@ async def test_status_propagation_with_retry_for_latest_attempt(inmemory_store:
updated_rollout = await inmemory_store.get_rollout_by_id(rollout.rollout_id)
assert updated_rollout is not None
assert updated_rollout.status == "queuing" # Should remain unchanged
assert updated_rollout.status == "preparing" # Should be status of attempt 2
# Fail attempt2 (latest) - should trigger retry since sequence_id=2 < max_attempts=3
await inmemory_store.update_attempt(
@@ -1708,7 +1708,7 @@ async def test_status_propagation_latest_changes_when_new_attempt_added(inmemory
updated_rollout = await inmemory_store.get_rollout_by_id(rollout.rollout_id)
assert updated_rollout is not None
assert updated_rollout.status == "succeeded" # Should remain unchanged
assert updated_rollout.status == "preparing" # Should be the status of attempt 2
# Update attempt2 (now latest) to failed
await inmemory_store.update_attempt(
+23 -2
View File
@@ -26,6 +26,7 @@ import multiprocessing
import os
import pprint
import re
import socket
import threading
import time
from contextlib import asynccontextmanager, contextmanager
@@ -101,12 +102,14 @@ class MockOpenAICompatibleServer:
Now supports replaying from prompt caches.
"""
def __init__(self, host: str = "127.0.0.1", port: int = 58000) -> None:
def __init__(self, host: str = "127.0.0.1", port: Optional[int] = None) -> None:
self.host = host
self.port = port
self._requested_port = port
self.port: Optional[int] = port
self.app = FastAPI()
self.server_thread = None
self.server = None
self._prev_openai_base_url: Optional[str] = None
self.prompt_caches = self._load_prompt_caches()
self._setup_routes()
@@ -167,8 +170,17 @@ class MockOpenAICompatibleServer:
return cached_response
raise ValueError("No suitable cached response found. Please ensure the prompt caches are populated.")
def _resolve_port(self) -> int:
if self._requested_port:
return self._requested_port
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind((self.host, 0))
return sock.getsockname()[1]
async def __aenter__(self):
# Start the server manually
self.port = self._resolve_port()
config = uvicorn.Config(self.app, host=self.host, port=self.port, log_level="error")
self.server = uvicorn.Server(config)
self.server_thread = threading.Thread(target=self.server.run, daemon=True)
@@ -184,6 +196,11 @@ class MockOpenAICompatibleServer:
if not getattr(self.server, "started", False):
raise RuntimeError("Server failed to start within timeout")
# Update the module-level base URL so downstream clients use the live port.
global OPENAI_BASE_URL
self._prev_openai_base_url = OPENAI_BASE_URL
OPENAI_BASE_URL = f"http://{self.host}:{self.port}/v1"
return self
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
@@ -191,6 +208,10 @@ class MockOpenAICompatibleServer:
self.server.should_exit = True
if self.server_thread and self.server_thread.is_alive():
self.server_thread.join(timeout=5)
if self._prev_openai_base_url is not None:
global OPENAI_BASE_URL
OPENAI_BASE_URL = self._prev_openai_base_url
self._prev_openai_base_url = None
async def run_agent(agent_func: Callable[[], Any]) -> None:
Generated
+137 -21
View File
File diff suppressed because one or more lines are too long