vanishingradient's picture
Shaped rewards + root listing in reset
8e4528c verified
Raw History Blame Contribute Delete
36.5 kB
"""TRL GRPO `environment_factory` for deepagents SWE-bench sandboxes.
Kaggle has no Docker for instance images — `get_reward` POSTs the model patch
to `SWE_GRADER_URL` (local `scripts/swe_grader_server.py` + tunnel).
Each rollout opens a Langfuse agent trace; every tool call is a nested `tool`
span; grading is an `evaluator` span with reward scores.
"""
from __future__ import annotations
import json
import os
import shutil
import tempfile
import traceback
import uuid
from functools import wraps
from pathlib import Path
from typing import Any, Callable
def _vlog(msg: str, **fields: Any) -> None:
"""Always-on verbose progress (Kaggle logs + local stderr)."""
extra = ""
if fields:
try:
extra = " | " + json.dumps(fields, default=str)[:1200]
except Exception:
extra = f" | {fields!r}"[:1200]
line = f"[darl] {msg}{extra}"
print(line, flush=True)
try:
from deepagents_rl.observability.logging import step
step("swe_env", msg, **fields)
except Exception:
pass
def _result_to_str(obj: Any) -> str:
if obj is None:
return ""
if isinstance(obj, str):
return obj[:8000]
if hasattr(obj, "output") and isinstance(obj.output, str):
return obj.output[:8000]
# Prefer listing entries before error (LsResult / GlobResult)
entries = getattr(obj, "entries", None)
if entries is not None:
lines: list[str] = []
for e in entries:
if isinstance(e, dict):
lines.append(str(e.get("path") or e))
else:
lines.append(str(e))
return ("\n".join(lines) if lines else "(empty directory)")[:8000]
matches = getattr(obj, "matches", None)
if matches is not None and not getattr(obj, "error", None):
try:
return json.dumps(matches, default=str)[:8000]
except Exception:
return str(matches)[:8000]
if hasattr(obj, "error") and obj.error:
return f"ERROR: {obj.error}"[:8000]
try:
return json.dumps(obj, default=str)[:8000]
except Exception:
return str(obj)[:8000]
# Common dirs Qwen invents that most SWE-bench repos do not have.
_PHANTOM_DIRS = frozenset(
{
"src",
"lib",
"app",
"source",
"sources",
"code",
"project",
"workspace",
}
)
def _as_str_list(val: Any) -> list[str]:
if val is None:
return []
if isinstance(val, list):
return [str(x) for x in val]
if isinstance(val, str):
s = val.strip()
if not s:
return []
try:
parsed = json.loads(s)
if isinstance(parsed, list):
return [str(x) for x in parsed]
except json.JSONDecodeError:
pass
return [s]
return [str(val)]
def _truncate(obj: Any, n: int = 4000) -> Any:
if isinstance(obj, str):
return obj[:n]
if isinstance(obj, dict):
return {k: _truncate(v, n) for k, v in list(obj.items())[:40]}
if isinstance(obj, list):
return [_truncate(v, n) for v in obj[:40]]
return obj
def grade_patch_remote(
*,
instance_id: str,
docker_image: str,
eval_script: str,
fail_to_pass: list[str],
pass_to_pass: list[str],
model_patch: str,
timeout_s: int = 1800,
) -> dict[str, Any]:
"""POST patch to the local Docker grader service."""
import requests
base = (os.environ.get("SWE_GRADER_URL") or "").rstrip("/")
if not base:
raise RuntimeError("SWE_GRADER_URL is not set (point at the local grader tunnel)")
headers = {
"Content-Type": "application/json",
"ngrok-skip-browser-warning": "1",
}
token = os.environ.get("SWE_GRADER_TOKEN") or ""
if token:
headers["Authorization"] = f"Bearer {token}"
payload = {
"instance_id": instance_id,
"image": docker_image,
"eval_script": eval_script,
"fail_to_pass": fail_to_pass,
"pass_to_pass": pass_to_pass,
"model_patch": model_patch or "",
"timeout_s": timeout_s,
}
_vlog(
"grader.POST /grade",
url=base,
instance_id=instance_id,
patch_bytes=len((model_patch or "").encode("utf-8")),
timeout_s=timeout_s,
)
resp = requests.post(f"{base}/grade", headers=headers, json=payload, timeout=timeout_s + 60)
_vlog("grader.response", status=resp.status_code, body_preview=(resp.text or "")[:400])
resp.raise_for_status()
data = resp.json()
_vlog(
"grader.result",
instance_id=instance_id,
reward_total=data.get("reward_total"),
resolution=data.get("resolution"),
resolved=data.get("resolved"),
error=data.get("error"),
)
return data
def _traced_tool(method: Callable[..., str]) -> Callable[..., str]:
"""Wrap a public tool method so each call becomes a Langfuse tool span."""
@wraps(method)
def wrapper(self: "DeepAgentSWEEnv", *args: Any, **kwargs: Any) -> str:
return self._call_tool(method.__name__, method, args, kwargs)
return wrapper
class DeepAgentSWEEnv:
"""Per-rollout TRL environment: LocalShellBackend tools + remote Docker reward."""
def __init__(self) -> None:
self._sandbox_parent: Path | None = None
self._sandbox: Path | None = None
self._backend: Any = None
self._task: dict[str, Any] = {}
self._last_reward: float = 0.0
self._last_report: dict[str, Any] = {}
self._rollout_id: str = ""
self._tool_calls: list[dict[str, Any]] = []
self._lf: Any = None
self._root_cm: Any = None
self._root: Any = None
self._trace_id: str = ""
self._root_span_id: str = ""
def _langfuse_enabled(self) -> bool:
if os.getenv("DARL_LANGFUSE", "1").strip() in ("0", "false", "no"):
return False
return bool(os.getenv("LANGFUSE_PUBLIC_KEY") and os.getenv("LANGFUSE_SECRET_KEY"))
def _ensure_langfuse(self) -> Any | None:
if not self._langfuse_enabled():
return None
if self._lf is not None:
return self._lf
try:
from langfuse import Langfuse, get_client
Langfuse(
public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
secret_key=os.environ["LANGFUSE_SECRET_KEY"],
host=os.getenv("LANGFUSE_HOST") or os.getenv("LANGFUSE_BASE_URL"),
)
self._lf = get_client()
return self._lf
except Exception as e: # noqa: BLE001
print(f"[langfuse] init failed: {e}")
return None
def _child_trace_context(self) -> dict[str, str] | None:
"""Attach children to THIS rollout's root — never ambient OTEL context.
Passing only ``trace_id`` (no parent) makes the Langfuse SDK invent a
NonRecordingSpan parent id → orphan parents + mega-trace pollution when
parallel GRPO rollouts share get_client().
"""
if not self._trace_id or not self._root_span_id:
return None
return {"trace_id": self._trace_id, "parent_span_id": self._root_span_id}
def _end_trace(self) -> None:
if self._root_cm is not None:
try:
self._root_cm.__exit__(None, None, None)
except Exception:
pass
self._root_cm = None
self._root = None
self._root_span_id = ""
# Keep _trace_id until next reset for logging; clear after flush.
if self._lf is not None:
try:
self._lf.flush()
except Exception:
pass
def _call_tool(
self,
name: str,
method: Callable[..., str],
args: tuple[Any, ...],
kwargs: dict[str, Any],
) -> str:
payload = _truncate({"args": list(args), "kwargs": kwargs})
n = len(self._tool_calls) + 1
_vlog(
f"tool[{n}].begin {name}",
instance_id=self._task.get("instance_id"),
rollout_id=self._rollout_id,
input=payload,
)
lf = self._lf
ctx = self._child_trace_context()
if lf is None or ctx is None:
try:
out = method(self, *args, **kwargs)
except Exception as e: # noqa: BLE001
out = f"TOOL_ERROR: {type(e).__name__}: {e}"
self._tool_calls.append({"name": name, "input": payload, "output": _truncate(out)})
_vlog(
f"tool[{n}].end {name}",
output_preview=str(out)[:500],
output_chars=len(str(out)),
)
return out
with lf.start_as_current_observation(
name=name,
as_type="tool",
trace_context=ctx,
input=payload,
metadata={
"rollout_id": self._rollout_id,
"instance_id": self._task.get("instance_id"),
"harness": "deepagents-swe-grpo",
"tool_index": n,
"trace_id": self._trace_id,
},
) as span:
try:
out = method(self, *args, **kwargs)
span.update(output=_truncate(out))
except Exception as e: # noqa: BLE001
out = f"TOOL_ERROR: {type(e).__name__}: {e}"
span.update(output=out, level="ERROR")
self._tool_calls.append({"name": name, "input": payload, "output": _truncate(out)})
_vlog(
f"tool[{n}].end {name}",
output_preview=str(out)[:500],
output_chars=len(str(out)),
)
return out
def reset(self, **kwargs: Any) -> str:
"""Materialize repo sandbox; return tool-forcing text TRL appends to the user message."""
self._cleanup()
self._end_trace()
instance_id = str(kwargs.get("instance_id") or kwargs.get("task_id") or "")
repo = str(kwargs.get("repo") or "")
base_commit = str(kwargs.get("base_commit") or "")
docker_image = str(kwargs.get("docker_image") or kwargs.get("image") or "")
eval_script = str(kwargs.get("eval_script") or "")
fail_to_pass = _as_str_list(
kwargs.get("fail_to_pass")
if kwargs.get("fail_to_pass") is not None
else kwargs.get("fail_to_pass_json")
)
pass_to_pass = _as_str_list(
kwargs.get("pass_to_pass")
if kwargs.get("pass_to_pass") is not None
else kwargs.get("pass_to_pass_json")
)
_vlog(
"env.reset.begin",
instance_id=instance_id,
repo=repo,
base_commit=(base_commit[:12] if base_commit else ""),
keys=list(kwargs.keys())[:30],
)
if not instance_id or not repo or not base_commit:
raise ValueError(
f"DeepAgentSWEEnv.reset requires instance_id/repo/base_commit; got keys={list(kwargs)}"
)
if not docker_image or not eval_script.strip():
raise ValueError(f"{instance_id}: missing docker_image/eval_script")
from deepagents.backends import LocalShellBackend
from deepagents_rl.harness.repo import materialize_repo_at_commit
self._rollout_id = str(uuid.uuid4())
self._tool_calls = []
self._task = {
"instance_id": instance_id,
"task_id": instance_id,
"repo": repo,
"base_commit": base_commit,
"image": docker_image,
"docker_image": docker_image,
"eval_script": eval_script,
"fail_to_pass": fail_to_pass,
"pass_to_pass": pass_to_pass,
}
self._last_reward = 0.0
self._last_report = {}
self._trace_id = ""
lf = self._ensure_langfuse()
_vlog("env.reset.langfuse", enabled=lf is not None)
if lf is not None:
try:
# IMPORTANT: do NOT pass trace_context={"trace_id": ...} alone.
# Langfuse SDK then fabricates a NonRecordingSpan parent → orphan
# parent ids in the UI and children from other rollouts can leak in.
# A plain start_as_current_observation creates a clean root trace.
self._root_cm = lf.start_as_current_observation(
name=f"grpo-swe-rollout:{instance_id}",
as_type="agent",
input={
"instance_id": instance_id,
"repo": repo,
"base_commit": base_commit,
"rollout_id": self._rollout_id,
"prompt_preview": _truncate(
kwargs.get("prompt")
if isinstance(kwargs.get("prompt"), str)
else (
kwargs.get("problem_statement")
or (kwargs.get("prompt") or "")
),
2000,
),
},
metadata={
"rollout_id": self._rollout_id,
"instance_id": instance_id,
"docker_image": docker_image,
"phase": "grpo_train_or_eval",
"harness": "deepagents-swe-grpo",
"session_id": self._rollout_id,
"tags": ["deepagents-rl", "swe-bench", "grpo", "tools"],
},
end_on_exit=True,
)
self._root = self._root_cm.__enter__()
self._trace_id = str(
getattr(self._root, "trace_id", None)
or getattr(self._root, "traceId", None)
or ""
)
self._root_span_id = str(
getattr(self._root, "id", None)
or getattr(self._root, "observation_id", None)
or ""
)
_vlog(
"env.reset.langfuse_trace",
trace_id=self._trace_id,
root_span_id=self._root_span_id,
)
except Exception as e: # noqa: BLE001
_vlog("env.reset.langfuse_failed", error=str(e))
self._root_cm = None
self._root = None
self._trace_id = ""
self._root_span_id = ""
def _materialize() -> None:
_vlog("env.reset.materialize", repo=repo, base_commit=base_commit[:12])
self._sandbox_parent = Path(
tempfile.mkdtemp(prefix=f"darl_grpo_{instance_id[:40]}_")
)
self._sandbox = self._sandbox_parent / "repo"
materialize_repo_at_commit(repo=repo, base_commit=base_commit, dest=self._sandbox)
self._backend = LocalShellBackend(
root_dir=str(self._sandbox),
virtual_mode=True,
timeout=60,
)
_vlog("env.reset.sandbox_ready", sandbox=str(self._sandbox))
ctx = self._child_trace_context()
if lf is not None and ctx is not None:
with lf.start_as_current_observation(
name="repo_materialize",
as_type="span",
trace_context=ctx,
input={"repo": repo, "base_commit": base_commit},
) as span:
_materialize()
span.update(output={"sandbox": str(self._sandbox)})
else:
_materialize()
# Pre-list root so the model sees real paths even before its first tool call.
root_listing = ""
try:
root_listing = self._root_listing()
except Exception as e: # noqa: BLE001
root_listing = f"(root listing failed: {e})"
_vlog(
"env.reset.done",
instance_id=instance_id,
rollout_id=self._rollout_id,
n_f2p=len(fail_to_pass),
n_p2p=len(pass_to_pass),
langfuse_trace_id=self._trace_id,
root_listing_chars=len(root_listing),
)
# TRL appends this string to the last user message — force tool use.
return (
"\n\nREPO ROOT LISTING (already gathered for you):\n"
f"{root_listing[:3500]}\n\n"
"CRITICAL REMINDER:\n"
"- You MUST use structured tool/function calls (not markdown ```json```).\n"
"- FIRST tool call: ls path='.' OR grep/glob/read_file on a REAL path from the listing above.\n"
"- Never invent src/lib/app. Prefer edit_file with a unique old_string.\n"
"- Text-only DONE with zero tools scores NEGATIVE reward. Call tools.\n"
"- After a minimal fix, stop tools and reply: DONE <summary>."
)
def _cleanup(self) -> None:
self._backend = None
if self._sandbox_parent is not None and self._sandbox_parent.exists():
shutil.rmtree(self._sandbox_parent, ignore_errors=True)
self._sandbox_parent = None
self._sandbox = None
def _require_backend(self) -> Any:
if self._backend is None:
raise RuntimeError("DeepAgentSWEEnv tool called before reset()")
return self._backend
@staticmethod
def _norm_path(path: str | None) -> str:
p = (path or ".").strip().strip("'\"")
if p in ("", "/", "./", ".\\", "~"):
return "."
# virtual_mode often wants leading slash for absolute-in-sandbox paths
if p.startswith("./"):
p = p[2:]
if p.startswith("/"):
p = p.lstrip("/") or "."
# Drop trailing slash for alias matching
p = p.rstrip("/") or "."
return p or "."
@classmethod
def _resolve_ls_path(cls, path: str | None) -> tuple[str, str | None]:
"""Map invented dirs (src/lib/…) to repo root before hitting the backend."""
p = cls._norm_path(path)
head = p.split("/", 1)[0].lower()
if head in _PHANTOM_DIRS:
return ".", p
return p, None
def _is_missing(self, result_obj: Any, text: str) -> bool:
err = getattr(result_obj, "error", None)
if err and "path_not_found" in str(err).lower():
return True
return "path_not_found" in (text or "").lower()
def _root_listing(self) -> str:
backend = self._require_backend()
# Prefer "/" then "." for virtual_mode backends
for candidate in ("/", "."):
result = backend.ls(candidate)
out = _result_to_str(result)
if not self._is_missing(result, out):
return out
return "(unable to list repo root)"
@_traced_tool
def ls(self, path: str = ".") -> str:
"""List files in a directory inside the sandbox repo root.
Args:
path: Directory relative to the repo root. Use "." for the root.
Do NOT assume a "src/" folder exists — many repos have none.
Returns:
Directory listing as text (falls back to repo root if path missing).
"""
backend = self._require_backend()
path, aliased_from = self._resolve_ls_path(path)
result = backend.ls(path)
out = _result_to_str(result)
if self._is_missing(result, out):
root = self._root_listing()
_vlog("tool.ls.fallback_root", requested=aliased_from or path)
return (
f"Path '{aliased_from or path}' does not exist here.\n"
f"Listing repo root instead (use these real paths):\n{root}"
)
if aliased_from and aliased_from != path:
_vlog("tool.ls.alias", requested=aliased_from, used=path)
return (
f"Note: '{aliased_from}' is not used in this repo; showing repo root:\n{out}"
)
return out
@_traced_tool
def glob(self, pattern: str, path: str = ".") -> str:
"""Glob for files matching pattern under path.
Args:
pattern: Glob pattern (e.g. '**/*.py').
path: Root path for the search (use '.' for repo root).
Returns:
Matching paths as text.
"""
backend = self._require_backend()
path, aliased_from = self._resolve_ls_path(path)
result = backend.glob(pattern, path)
out = _result_to_str(result)
if self._is_missing(result, out):
return (
f"Path '{aliased_from or path}' not found for glob; retrying at repo root.\n"
f"{_result_to_str(backend.glob(pattern, '.'))}"
)
return out
@_traced_tool
def grep(self, pattern: str, path: str = ".") -> str:
"""Search file contents for a regex pattern.
Args:
pattern: Regex pattern to search for.
path: Root path for the search (use '.' for repo root).
Returns:
Matching lines as text.
"""
backend = self._require_backend()
path, aliased_from = self._resolve_ls_path(path)
result = backend.grep(pattern, path)
out = _result_to_str(result)
if self._is_missing(result, out):
return (
f"Path '{aliased_from or path}' not found for grep; retrying at repo root.\n"
f"{_result_to_str(backend.grep(pattern, '.'))}"
)
return out
@_traced_tool
def read_file(self, file_path: str, offset: int = 0, limit: int = 200) -> str:
"""Read a text file from the sandbox.
Args:
file_path: Path relative to the repo root (discover via ls/glob first).
offset: Starting line offset.
limit: Max number of lines to return.
Returns:
File contents as text.
"""
backend = self._require_backend()
file_path = self._norm_path(file_path)
# If model invents src/foo.py, try stripping the phantom prefix once.
head = file_path.split("/", 1)[0].lower()
candidates = [file_path]
if head in _PHANTOM_DIRS and "/" in file_path:
candidates.append(file_path.split("/", 1)[1])
last_out = ""
for cand in candidates:
result = backend.read(cand, offset=offset, limit=limit)
out = _result_to_str(result)
last_out = out
if not self._is_missing(result, out):
if cand != file_path:
return f"(read via '{cand}' — '{file_path}' missing)\n{out}"
return out
return (
f"File '{file_path}' not found. Use ls path='.' or glob to discover real paths.\n"
f"Repo root listing:\n{self._root_listing()}\nLast error: {last_out}"
)
@_traced_tool
def write_file(self, file_path: str, content: str) -> str:
"""Write or create a text file in the sandbox.
Args:
file_path: Path relative to the repo root.
content: Full file contents to write.
Returns:
Write result as text.
"""
return _result_to_str(self._require_backend().write(file_path, content))
@_traced_tool
def edit_file(self, file_path: str, old_string: str, new_string: str) -> str:
"""Replace old_string with new_string in an existing file.
Args:
file_path: Path relative to the repo root.
old_string: Exact text to find.
new_string: Replacement text.
Returns:
Edit result as text.
"""
return _result_to_str(
self._require_backend().edit(file_path, old_string, new_string)
)
@_traced_tool
def execute(self, command: str) -> str:
"""Run a shell command in the sandbox.
Args:
command: Shell command (e.g. python -c '...').
Returns:
Command stdout/stderr as text.
"""
return _result_to_str(self._require_backend().execute(command))
def _shaped_reward(
self,
*,
docker_reward: float,
model_patch: str,
report: dict[str, Any],
) -> tuple[float, dict[str, float]]:
"""Dense process reward so GRPO sees non-zero variance before resolve.
Components (additive, then clamped):
- no tool calls: -0.15
- tool calls: +0.05 each (cap +0.25)
- used edit_file/write_file: +0.15
- non-empty git patch: +0.30
- docker/pass reward (0..1 typically): +1.0 * docker_reward
"""
names = [str(c.get("name") or "") for c in self._tool_calls]
n = len(names)
parts = {
"tool_penalty": -0.15 if n == 0 else 0.0,
"tool_bonus": 0.0 if n == 0 else float(min(0.25, 0.05 * n)),
"edit_bonus": 0.15
if any(x in names for x in ("edit_file", "write_file"))
else 0.0,
"patch_bonus": 0.30 if (model_patch or "").strip() else 0.0,
"docker": float(docker_reward or 0.0),
}
total = sum(parts.values())
total = max(-0.2, min(1.6, total))
parts["total"] = total
return total, parts
def get_reward(self) -> float:
"""Shaped reward = process bonuses + remote Docker SWE score (if patch)."""
lf = self._lf
reward = 0.0
report: dict[str, Any] = {}
shape_parts: dict[str, float] = {}
model_patch = ""
_vlog(
"env.get_reward.begin",
instance_id=self._task.get("instance_id"),
n_tool_calls=len(self._tool_calls),
tools_used=[c.get("name") for c in self._tool_calls],
)
def _grade() -> float:
nonlocal report, model_patch, shape_parts
docker_reward = 0.0
if self._sandbox is None:
report = {"error": "no_sandbox", "reward_total": 0.0}
_vlog("env.get_reward.no_sandbox")
shaped, shape_parts = self._shaped_reward(
docker_reward=0.0, model_patch="", report=report
)
report["shaped"] = shape_parts
report["reward_total"] = shaped
return shaped
from deepagents_rl.harness.repo import git_diff_patch
try:
_vlog("env.get_reward.git_diff")
model_patch = git_diff_patch(self._sandbox)
_vlog(
"env.get_reward.patch",
patch_bytes=len(model_patch.encode("utf-8")),
empty=not bool(model_patch.strip()),
preview=model_patch[:300],
)
except Exception as e: # noqa: BLE001
report = {"error": f"git_diff_failed: {e}"}
_vlog("env.get_reward.git_diff_failed", error=str(e))
shaped, shape_parts = self._shaped_reward(
docker_reward=0.0, model_patch="", report=report
)
report["shaped"] = shape_parts
report["reward_total"] = shaped
return shaped
if not model_patch.strip():
report = {"error": "empty_model_patch", "reward_total": 0.0}
_vlog("env.get_reward.empty_patch")
else:
try:
report = grade_patch_remote(
instance_id=self._task["instance_id"],
docker_image=self._task["docker_image"],
eval_script=self._task["eval_script"],
fail_to_pass=list(self._task["fail_to_pass"]),
pass_to_pass=list(self._task["pass_to_pass"]),
model_patch=model_patch,
)
docker_reward = float(report.get("reward_total") or 0.0)
except Exception as e: # noqa: BLE001
report = {
"error": f"grader_failed: {e}",
"traceback": traceback.format_exc()[-1500:],
}
_vlog("env.get_reward.grader_failed", error=str(e))
docker_reward = 0.0
shaped, shape_parts = self._shaped_reward(
docker_reward=docker_reward,
model_patch=model_patch,
report=report,
)
report["docker_reward"] = docker_reward
report["shaped"] = shape_parts
report["reward_total"] = shaped
_vlog("env.get_reward.shaped", **shape_parts)
return shaped
ctx = self._child_trace_context()
if lf is not None and ctx is not None:
with lf.start_as_current_observation(
name="reward_grader",
as_type="evaluator",
trace_context=ctx,
input={
"instance_id": self._task.get("instance_id"),
"n_tool_calls": len(self._tool_calls),
"tools_used": [c["name"] for c in self._tool_calls],
},
) as span:
reward = _grade()
span.update(
output=_truncate(
{
"reward_total": reward,
"docker_reward": report.get("docker_reward"),
"shaped": report.get("shaped"),
"pass_frac": report.get("pass_frac"),
"resolved": report.get("resolved"),
"resolution": report.get("resolution"),
"error": report.get("error"),
"n_tool_calls": len(self._tool_calls),
}
)
)
try:
lf.score_current_trace(name="reward_total", value=float(reward))
lf.score_current_trace(
name="pass_frac", value=float(report.get("pass_frac") or 0.0)
)
lf.score_current_trace(
name="resolved", value=1.0 if report.get("resolved") else 0.0
)
lf.score_current_trace(
name="n_tool_calls", value=float(len(self._tool_calls))
)
if isinstance(report.get("shaped"), dict):
for sk, sv in report["shaped"].items():
lf.score_current_trace(name=f"shaped_{sk}", value=float(sv))
except Exception:
pass
if self._root is not None:
try:
self._root.update(
output={
"reward_total": reward,
"n_tool_calls": len(self._tool_calls),
"tools_used": [c["name"] for c in self._tool_calls],
"resolution": report.get("resolution"),
"shaped": report.get("shaped"),
}
)
except Exception:
pass
else:
reward = _grade()
self._last_report = report
self._last_reward = reward
_vlog(
"env.get_reward.done",
instance_id=self._task.get("instance_id"),
reward_total=reward,
pass_frac=report.get("pass_frac"),
resolution=report.get("resolution"),
resolved=report.get("resolved"),
error=report.get("error"),
n_tool_calls=len(self._tool_calls),
shaped=report.get("shaped"),
)
# W&B: one point per completed rollout (train + ckpt eval)
try:
import wandb
if wandb.run is not None:
tools_used = [c.get("name") for c in self._tool_calls]
shaped = report.get("shaped") if isinstance(report.get("shaped"), dict) else {}
wandb.log(
{
"rollout/reward_total": float(reward),
"rollout/docker_reward": float(report.get("docker_reward") or 0.0),
"rollout/pass_frac": float(report.get("pass_frac") or 0.0),
"rollout/resolved": 1.0 if report.get("resolved") else 0.0,
"rollout/n_tool_calls": float(len(self._tool_calls)),
"rollout/instance_id": self._task.get("instance_id") or "",
"rollout/tools_used": ",".join(tools_used),
"rollout/resolution": str(report.get("resolution") or ""),
"rollout/grader_error": str(report.get("error") or ""),
"rollout/shaped_tool_bonus": float(shaped.get("tool_bonus") or 0.0),
"rollout/shaped_edit_bonus": float(shaped.get("edit_bonus") or 0.0),
"rollout/shaped_patch_bonus": float(shaped.get("patch_bonus") or 0.0),
}
)
_vlog("env.get_reward.wandb_logged")
except Exception as e: # noqa: BLE001
_vlog("env.get_reward.wandb_failed", error=str(e))
self._end_trace()
_vlog("env.get_reward.trace_closed", trace_id=self._trace_id)
return reward
def prepare_swe_grpo_rows(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Normalize HF SWE Lite rows for TRL GRPO + DeepAgentSWEEnv.
Renames `image` -> `docker_image` so TRL does not treat it as multimodal.
Converts `prompt` to conversational chat messages (required for tool calling).
"""
from deepagents_rl.data.swe_bench_lite import build_agent_prompt, from_hf_record
rows: list[dict[str, Any]] = []
for rec in records:
if "fail_to_pass_json" in rec or (
"prompt" in rec and isinstance(rec.get("prompt"), str) and "image" in rec
):
task = from_hf_record(rec)
else:
task = dict(rec)
# Rebuild prompt locally so tool-forcing text stays current (HF rows may be stale).
messages = [
{
"role": "user",
"content": build_agent_prompt(
str(task.get("problem_statement") or ""),
instance_id=str(task.get("instance_id") or task.get("task_id") or ""),
),
}
]
docker_image = str(task.get("docker_image") or task.get("image") or "")
rows.append(
{
"prompt": messages,
"task_id": task["task_id"],
"instance_id": task["instance_id"],
"repo": task["repo"],
"base_commit": task["base_commit"],
"docker_image": docker_image,
"eval_script": task.get("eval_script") or "",
"fail_to_pass": list(task.get("fail_to_pass") or []),
"pass_to_pass": list(task.get("pass_to_pass") or []),
"problem_statement": task.get("problem_statement") or "",
}
)
return rows