Files
wehub-resource-sync 85742ab165
Deploy Documentation / deploy (push) Has been cancelled
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, latest, Python 3.13) (push) Has been cancelled
Dashboard / Chromatic (push) Has been cancelled
CPU Test / Lint - fast (push) Has been cancelled
CPU Test / Lint - next (push) Has been cancelled
CPU Test / Lint - slow (push) Has been cancelled
CPU Test / Lint - JavaScript (push) Has been cancelled
CPU Test / Build documentation (push) Has been cancelled
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Others, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Store, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Weave, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Others, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Store, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Utilities, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (JavaScript) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:44:17 +08:00

310 lines
14 KiB
Python

# Copyright (c) Microsoft. All rights reserved.
import json
import logging
import time
from typing import Any, Dict, List, Optional, cast
from opentelemetry.sdk.trace import ReadableSpan
from agentlightning.adapter import TracerTraceToTriplet
from agentlightning.client import AgentLightningClient
from agentlightning.litagent import LitAgent
from agentlightning.litagent.litagent import is_v0_1_rollout_api
from agentlightning.tracer.base import Tracer
from agentlightning.types import RolloutLegacy, RolloutRawResultLegacy, Span, SpanLike, Triplet
from .base import Runner
logger = logging.getLogger(__name__)
__all__ = [
"LegacyAgentRunner",
]
class LegacyAgentRunner(Runner[Any]):
"""Manages the agent's execution loop and integrates with AgentOps.
This class orchestrates the interaction between the agent (`LitAgent`) and
the server (`AgentLightningClient`). It handles polling for tasks, executing
the agent's logic, and reporting results back to the server. If enabled,
it will also automatically trace each rollout using AgentOps.
Attributes:
agent: The `LitAgent` instance containing the agent's logic.
client: The `AgentLightningClient` for server communication.
tracer: The tracer instance for this runner/worker.
worker_id: An optional identifier for the worker process.
max_tasks: The maximum number of tasks to process before stopping.
"""
def __init__(
self,
agent: LitAgent[Any],
client: AgentLightningClient,
tracer: Tracer,
triplet_exporter: TracerTraceToTriplet,
worker_id: Optional[int] = None,
max_tasks: Optional[int] = None,
):
super().__init__()
self.agent = agent
self.client = client
self.tracer = tracer
self.triplet_exporter = triplet_exporter
# Worker-specific attributes
self.worker_id = worker_id
self.max_tasks = max_tasks
# These methods are overridden by Runner, getting them back to old behavior.
def init(self, *args: Any, **kwargs: Any) -> None:
pass
def init_worker(self, worker_id: int, *args: Any, **kwargs: Any) -> None:
self.worker_id = worker_id
def teardown_worker(self, worker_id: int, *args: Any, **kwargs: Any) -> None:
pass
def teardown(self, *args: Any, **kwargs: Any) -> None:
pass
def _log_prefix(self, rollout_id: Optional[str] = None) -> str:
"""Generates a standardized log prefix for the current worker."""
if self.worker_id is not None:
if rollout_id:
return f"[Worker {self.worker_id} | RolloutLegacy {rollout_id}]"
else:
return f"[Worker {self.worker_id}]"
if rollout_id:
return f"[RolloutLegacy {rollout_id}]"
return "[Default Worker]"
def _to_rollout_object(
self,
result: RolloutRawResultLegacy,
rollout_id: str,
) -> RolloutLegacy:
"""Standardizes the agent's return value into a RolloutLegacy object.
Args:
result: The output from the agent's rollout method.
rollout_id: The unique identifier for the current task.
Returns:
A standardized `RolloutLegacy` object for reporting to the server.
"""
trace: Any = None
final_reward: Optional[float] = None
triplets: Optional[List[Triplet]] = None
trace_spans: Optional[List[SpanLike]] = None
# Handle different types of results from the agent
# Case 1: result is a float (final reward)
if isinstance(result, float):
final_reward = result
# Case 2: result is a list of Triplets
if isinstance(result, list) and all(isinstance(t, Triplet) for t in result):
triplets = result # type: ignore
# Case 3.1: result is a list of ReadableSpan (OpenTelemetry spans)
if isinstance(result, list) and all(isinstance(t, (ReadableSpan)) for t in result):
trace_spans = result # type: ignore
trace = [json.loads(readable_span.to_json()) for readable_span in trace_spans] # type: ignore
# Case 3.2: result is a list of Span (Agent-lightning spans)
if isinstance(result, list) and all(isinstance(t, Span) for t in result):
trace_spans = result # type: ignore
trace = [span.model_dump() for span in trace_spans] # type: ignore
# Case 4: result is a list of dict (trace JSON)
if isinstance(result, list) and all(isinstance(t, dict) for t in result):
trace = result
# Case 5: result is a RolloutLegacy object
if isinstance(result, RolloutLegacy):
final_reward = result.final_reward
triplets = result.triplets
trace = result.trace
# If the agent has tracing enabled, use the tracer's last trace if not already set
if self.tracer and (trace is None or trace_spans is None):
trace_spans = self.tracer.get_last_trace() # type: ignore
if trace_spans:
trace = [cast(Span, span).model_dump() for span in trace_spans]
# Always extract triplets from the trace using TracerTraceToTriplet
if trace_spans:
triplets = self.triplet_exporter(trace_spans) # type: ignore
# If the agent has triplets, use the last one for final reward if not set
if triplets and triplets[-1].reward is not None and final_reward is None:
final_reward = triplets[-1].reward
# Create the RolloutLegacy object with standardized fields
result_dict: Dict[str, Any] = {
"rollout_id": rollout_id,
}
if final_reward is not None:
result_dict["final_reward"] = final_reward
if triplets is not None:
result_dict["triplets"] = triplets
if trace is not None:
result_dict["trace"] = trace
if isinstance(result, RolloutLegacy):
return result.model_copy(update=result_dict)
return RolloutLegacy(**result_dict)
def run(self) -> bool: # type: ignore
"""Poll the task and rollout once synchronously."""
self.agent.set_runner(self) # Ensure the agent has a reference to this runner
task = self.client.poll_next_task()
if task is None:
logger.info(f"{self._log_prefix()} Poll returned no task. Exiting.")
return False
rollout_id = task.rollout_id
resources_id = task.resources_id
resources_update = None
if resources_id:
resources_update = self.client.get_resources_by_id(resources_id)
else:
logger.debug(f"{self._log_prefix(rollout_id)} No 'resources_id'. Fetching latest resources.")
resources_update = self.client.get_latest_resources()
if not resources_update:
logger.error(f"{self._log_prefix(rollout_id)} Failed to fetch resources. Skipping.")
return False
rollout_obj = RolloutLegacy(rollout_id=task.rollout_id, task=task) # Default empty rollout
try:
try:
self.agent.on_rollout_start(task, self, self.tracer)
except Exception:
logger.exception(f"{self._log_prefix(rollout_id)} Exception during on_rollout_start hook.")
with self.tracer._trace_context_sync(name=f"rollout_{rollout_id}"): # pyright: ignore[reportPrivateUsage]
start_time = time.time()
rollout_method = self.agent.training_rollout if task.mode == "train" else self.agent.validation_rollout
# Pass the task input, not the whole task object
if is_v0_1_rollout_api(rollout_method):
result = cast(
RolloutRawResultLegacy,
rollout_method(
task.input, rollout_id=rollout_obj.rollout_id, resources=resources_update.resources # type: ignore
),
) # type: ignore
else:
result = rollout_method(task.input, resources=resources_update.resources, rollout=rollout_obj) # type: ignore
rollout_obj = self._to_rollout_object(result, task.rollout_id) # type: ignore
end_time = time.time()
logger.info(
f"{self._log_prefix(rollout_id)} Completed in "
f"{end_time - start_time:.2f}s. Triplet length: "
f"{len(rollout_obj.triplets) if rollout_obj.triplets is not None else 'N/A'}. "
f"Reward: {rollout_obj.final_reward}"
)
except Exception:
logger.exception(f"{self._log_prefix(rollout_id)} Exception during rollout.")
finally:
try:
self.agent.on_rollout_end(task, rollout_obj, self, self.tracer) # type: ignore
except Exception:
logger.exception(f"{self._log_prefix(rollout_id)} Exception during on_rollout_end hook.")
self.client.post_rollout(rollout_obj)
return True
def iter(self) -> int: # type: ignore
"""Executes the synchronous polling and rollout loop."""
num_tasks_processed = 0
logger.info(f"{self._log_prefix()} Started sync rollouts (max: {self.max_tasks or 'unlimited'}).")
while self.max_tasks is None or num_tasks_processed < self.max_tasks:
if self.run():
num_tasks_processed += 1
if num_tasks_processed % 10 == 0 or num_tasks_processed == 1:
logger.info(f"{self._log_prefix()} Progress: {num_tasks_processed}/{self.max_tasks or 'unlimited'}")
logger.info(f"{self._log_prefix()} Finished sync rollouts. Processed {num_tasks_processed} tasks.")
return num_tasks_processed
async def run_async(self) -> bool:
"""Poll the task and rollout once."""
self.agent.set_runner(self) # Ensure the agent has a reference to this runner
task = await self.client.poll_next_task_async()
if task is None:
logger.info(f"{self._log_prefix()} Poll returned no task. Exiting.")
return False
rollout_id = task.rollout_id
resources_id = task.resources_id
resources_update = None
if resources_id:
resources_update = await self.client.get_resources_by_id_async(resources_id)
else:
logger.debug(f"{self._log_prefix(rollout_id)} No 'resources_id'. Fetching latest resources.")
resources_update = await self.client.get_latest_resources_async()
if not resources_update:
logger.error(f"{self._log_prefix(rollout_id)} Failed to fetch resources. Skipping.")
return False
rollout_obj = RolloutLegacy(rollout_id=task.rollout_id, task=task) # Default empty rollout
try:
try:
self.agent.on_rollout_start(task, self, self.tracer)
except Exception:
logger.exception(f"{self._log_prefix(rollout_id)} Exception during on_rollout_start hook.")
async with self.tracer.trace_context(name=f"rollout_{rollout_id}"):
start_time = time.time()
rollout_method = (
self.agent.training_rollout_async if task.mode == "train" else self.agent.validation_rollout_async
)
# Pass the task input, not the whole task object
if is_v0_1_rollout_api(rollout_method):
result = cast(
RolloutRawResultLegacy,
await rollout_method(
task.input, rollout_id=rollout_obj.rollout_id, resources=resources_update.resources # type: ignore
),
) # type: ignore
else:
result = await rollout_method(task.input, resources=resources_update.resources, rollout=rollout_obj) # type: ignore
rollout_obj = self._to_rollout_object(result, task.rollout_id) # type: ignore
end_time = time.time()
logger.info(
f"{self._log_prefix(rollout_id)} Completed in "
f"{end_time - start_time:.2f}s. Triplet length: "
f"{len(rollout_obj.triplets) if rollout_obj.triplets is not None else 'N/A'}. "
f"Reward: {rollout_obj.final_reward}"
)
except Exception:
logger.exception(f"{self._log_prefix(rollout_id)} Exception during rollout.")
finally:
try:
self.agent.on_rollout_end(task, rollout_obj, self, self.tracer) # type: ignore
except Exception:
logger.exception(f"{self._log_prefix(rollout_id)} Exception during on_rollout_end hook.")
await self.client.post_rollout_async(rollout_obj)
return True
async def iter_async(self) -> int:
"""Executes the asynchronous polling and rollout loop."""
num_tasks_processed = 0
logger.info(f"{self._log_prefix()} Started async rollouts (max: {self.max_tasks or 'unlimited'}).")
while self.max_tasks is None or num_tasks_processed < self.max_tasks:
if await self.run_async():
num_tasks_processed += 1
if num_tasks_processed % 10 == 0 or num_tasks_processed == 1:
logger.info(f"{self._log_prefix()} Progress: {num_tasks_processed}/{self.max_tasks or 'unlimited'}")
logger.info(f"{self._log_prefix()} Finished async rollouts. Processed {num_tasks_processed} tasks.")
return num_tasks_processed