vanishingradient commited on
Commit
8e4528c
·
verified ·
1 Parent(s): d5f40a1

Shaped rewards + root listing in reset

Browse files
Files changed (1) hide show
  1. src/deepagents_rl/train/swe_env.py +104 -30
src/deepagents_rl/train/swe_env.py CHANGED
@@ -447,6 +447,12 @@ class DeepAgentSWEEnv:
447
  span.update(output={"sandbox": str(self._sandbox)})
448
  else:
449
  _materialize()
 
 
 
 
 
 
450
  _vlog(
451
  "env.reset.done",
452
  instance_id=instance_id,
@@ -454,16 +460,18 @@ class DeepAgentSWEEnv:
454
  n_f2p=len(fail_to_pass),
455
  n_p2p=len(pass_to_pass),
456
  langfuse_trace_id=self._trace_id,
 
457
  )
458
  # TRL appends this string to the last user message — force tool use.
459
  return (
460
- "\n\nCRITICAL REMINDER (edge cases):\n"
461
- "- Use structured tool/function calls only (not markdown ```json``` blobs).\n"
462
- "- FIRST tool call MUST be ls with path='.' (repo root). Never ls 'src'/'lib'/'app'.\n"
463
- "- Only read_file/edit_file paths that appeared in ls/glob output.\n"
464
- "- Prefer edit_file with unique old_string; keep the diff minimal.\n"
465
- "- If a path is missing, ls '.' again and pivot — do not retry the same missing path.\n"
466
- "- When done editing, stop tools and reply: DONE <summary>."
 
467
  )
468
 
469
  def _cleanup(self) -> None:
@@ -664,11 +672,45 @@ class DeepAgentSWEEnv:
664
  """
665
  return _result_to_str(self._require_backend().execute(command))
666
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
667
  def get_reward(self) -> float:
668
- """Export git diff and grade via remote Docker SWE evaluator."""
669
  lf = self._lf
670
  reward = 0.0
671
  report: dict[str, Any] = {}
 
 
672
  _vlog(
673
  "env.get_reward.begin",
674
  instance_id=self._task.get("instance_id"),
@@ -677,11 +719,17 @@ class DeepAgentSWEEnv:
677
  )
678
 
679
  def _grade() -> float:
680
- nonlocal report
 
681
  if self._sandbox is None:
682
  report = {"error": "no_sandbox", "reward_total": 0.0}
683
  _vlog("env.get_reward.no_sandbox")
684
- return 0.0
 
 
 
 
 
685
 
686
  from deepagents_rl.harness.repo import git_diff_patch
687
 
@@ -697,31 +745,45 @@ class DeepAgentSWEEnv:
697
  except Exception as e: # noqa: BLE001
698
  report = {"error": f"git_diff_failed: {e}"}
699
  _vlog("env.get_reward.git_diff_failed", error=str(e))
700
- return 0.0
 
 
 
 
 
701
 
702
  if not model_patch.strip():
703
  report = {"error": "empty_model_patch", "reward_total": 0.0}
704
  _vlog("env.get_reward.empty_patch")
705
- return 0.0
706
-
707
- try:
708
- report = grade_patch_remote(
709
- instance_id=self._task["instance_id"],
710
- docker_image=self._task["docker_image"],
711
- eval_script=self._task["eval_script"],
712
- fail_to_pass=list(self._task["fail_to_pass"]),
713
- pass_to_pass=list(self._task["pass_to_pass"]),
714
- model_patch=model_patch,
715
- )
716
- except Exception as e: # noqa: BLE001
717
- report = {
718
- "error": f"grader_failed: {e}",
719
- "traceback": traceback.format_exc()[-1500:],
720
- }
721
- _vlog("env.get_reward.grader_failed", error=str(e))
722
- return 0.0
723
 
724
- return float(report.get("reward_total") or 0.0)
 
 
 
 
 
 
 
 
 
725
 
726
  ctx = self._child_trace_context()
727
  if lf is not None and ctx is not None:
@@ -740,6 +802,8 @@ class DeepAgentSWEEnv:
740
  output=_truncate(
741
  {
742
  "reward_total": reward,
 
 
743
  "pass_frac": report.get("pass_frac"),
744
  "resolved": report.get("resolved"),
745
  "resolution": report.get("resolution"),
@@ -759,6 +823,9 @@ class DeepAgentSWEEnv:
759
  lf.score_current_trace(
760
  name="n_tool_calls", value=float(len(self._tool_calls))
761
  )
 
 
 
762
  except Exception:
763
  pass
764
  if self._root is not None:
@@ -769,6 +836,7 @@ class DeepAgentSWEEnv:
769
  "n_tool_calls": len(self._tool_calls),
770
  "tools_used": [c["name"] for c in self._tool_calls],
771
  "resolution": report.get("resolution"),
 
772
  }
773
  )
774
  except Exception:
@@ -787,6 +855,7 @@ class DeepAgentSWEEnv:
787
  resolved=report.get("resolved"),
788
  error=report.get("error"),
789
  n_tool_calls=len(self._tool_calls),
 
790
  )
791
 
792
  # W&B: one point per completed rollout (train + ckpt eval)
@@ -795,9 +864,11 @@ class DeepAgentSWEEnv:
795
 
796
  if wandb.run is not None:
797
  tools_used = [c.get("name") for c in self._tool_calls]
 
798
  wandb.log(
799
  {
800
  "rollout/reward_total": float(reward),
 
801
  "rollout/pass_frac": float(report.get("pass_frac") or 0.0),
802
  "rollout/resolved": 1.0 if report.get("resolved") else 0.0,
803
  "rollout/n_tool_calls": float(len(self._tool_calls)),
@@ -805,6 +876,9 @@ class DeepAgentSWEEnv:
805
  "rollout/tools_used": ",".join(tools_used),
806
  "rollout/resolution": str(report.get("resolution") or ""),
807
  "rollout/grader_error": str(report.get("error") or ""),
 
 
 
808
  }
809
  )
810
  _vlog("env.get_reward.wandb_logged")
 
447
  span.update(output={"sandbox": str(self._sandbox)})
448
  else:
449
  _materialize()
450
+ # Pre-list root so the model sees real paths even before its first tool call.
451
+ root_listing = ""
452
+ try:
453
+ root_listing = self._root_listing()
454
+ except Exception as e: # noqa: BLE001
455
+ root_listing = f"(root listing failed: {e})"
456
  _vlog(
457
  "env.reset.done",
458
  instance_id=instance_id,
 
460
  n_f2p=len(fail_to_pass),
461
  n_p2p=len(pass_to_pass),
462
  langfuse_trace_id=self._trace_id,
463
+ root_listing_chars=len(root_listing),
464
  )
465
  # TRL appends this string to the last user message — force tool use.
466
  return (
467
+ "\n\nREPO ROOT LISTING (already gathered for you):\n"
468
+ f"{root_listing[:3500]}\n\n"
469
+ "CRITICAL REMINDER:\n"
470
+ "- You MUST use structured tool/function calls (not markdown ```json```).\n"
471
+ "- FIRST tool call: ls path='.' OR grep/glob/read_file on a REAL path from the listing above.\n"
472
+ "- Never invent src/lib/app. Prefer edit_file with a unique old_string.\n"
473
+ "- Text-only DONE with zero tools scores NEGATIVE reward. Call tools.\n"
474
+ "- After a minimal fix, stop tools and reply: DONE <summary>."
475
  )
476
 
477
  def _cleanup(self) -> None:
 
672
  """
673
  return _result_to_str(self._require_backend().execute(command))
674
 
675
+ def _shaped_reward(
676
+ self,
677
+ *,
678
+ docker_reward: float,
679
+ model_patch: str,
680
+ report: dict[str, Any],
681
+ ) -> tuple[float, dict[str, float]]:
682
+ """Dense process reward so GRPO sees non-zero variance before resolve.
683
+
684
+ Components (additive, then clamped):
685
+ - no tool calls: -0.15
686
+ - tool calls: +0.05 each (cap +0.25)
687
+ - used edit_file/write_file: +0.15
688
+ - non-empty git patch: +0.30
689
+ - docker/pass reward (0..1 typically): +1.0 * docker_reward
690
+ """
691
+ names = [str(c.get("name") or "") for c in self._tool_calls]
692
+ n = len(names)
693
+ parts = {
694
+ "tool_penalty": -0.15 if n == 0 else 0.0,
695
+ "tool_bonus": 0.0 if n == 0 else float(min(0.25, 0.05 * n)),
696
+ "edit_bonus": 0.15
697
+ if any(x in names for x in ("edit_file", "write_file"))
698
+ else 0.0,
699
+ "patch_bonus": 0.30 if (model_patch or "").strip() else 0.0,
700
+ "docker": float(docker_reward or 0.0),
701
+ }
702
+ total = sum(parts.values())
703
+ total = max(-0.2, min(1.6, total))
704
+ parts["total"] = total
705
+ return total, parts
706
+
707
  def get_reward(self) -> float:
708
+ """Shaped reward = process bonuses + remote Docker SWE score (if patch)."""
709
  lf = self._lf
710
  reward = 0.0
711
  report: dict[str, Any] = {}
712
+ shape_parts: dict[str, float] = {}
713
+ model_patch = ""
714
  _vlog(
715
  "env.get_reward.begin",
716
  instance_id=self._task.get("instance_id"),
 
719
  )
720
 
721
  def _grade() -> float:
722
+ nonlocal report, model_patch, shape_parts
723
+ docker_reward = 0.0
724
  if self._sandbox is None:
725
  report = {"error": "no_sandbox", "reward_total": 0.0}
726
  _vlog("env.get_reward.no_sandbox")
727
+ shaped, shape_parts = self._shaped_reward(
728
+ docker_reward=0.0, model_patch="", report=report
729
+ )
730
+ report["shaped"] = shape_parts
731
+ report["reward_total"] = shaped
732
+ return shaped
733
 
734
  from deepagents_rl.harness.repo import git_diff_patch
735
 
 
745
  except Exception as e: # noqa: BLE001
746
  report = {"error": f"git_diff_failed: {e}"}
747
  _vlog("env.get_reward.git_diff_failed", error=str(e))
748
+ shaped, shape_parts = self._shaped_reward(
749
+ docker_reward=0.0, model_patch="", report=report
750
+ )
751
+ report["shaped"] = shape_parts
752
+ report["reward_total"] = shaped
753
+ return shaped
754
 
755
  if not model_patch.strip():
756
  report = {"error": "empty_model_patch", "reward_total": 0.0}
757
  _vlog("env.get_reward.empty_patch")
758
+ else:
759
+ try:
760
+ report = grade_patch_remote(
761
+ instance_id=self._task["instance_id"],
762
+ docker_image=self._task["docker_image"],
763
+ eval_script=self._task["eval_script"],
764
+ fail_to_pass=list(self._task["fail_to_pass"]),
765
+ pass_to_pass=list(self._task["pass_to_pass"]),
766
+ model_patch=model_patch,
767
+ )
768
+ docker_reward = float(report.get("reward_total") or 0.0)
769
+ except Exception as e: # noqa: BLE001
770
+ report = {
771
+ "error": f"grader_failed: {e}",
772
+ "traceback": traceback.format_exc()[-1500:],
773
+ }
774
+ _vlog("env.get_reward.grader_failed", error=str(e))
775
+ docker_reward = 0.0
776
 
777
+ shaped, shape_parts = self._shaped_reward(
778
+ docker_reward=docker_reward,
779
+ model_patch=model_patch,
780
+ report=report,
781
+ )
782
+ report["docker_reward"] = docker_reward
783
+ report["shaped"] = shape_parts
784
+ report["reward_total"] = shaped
785
+ _vlog("env.get_reward.shaped", **shape_parts)
786
+ return shaped
787
 
788
  ctx = self._child_trace_context()
789
  if lf is not None and ctx is not None:
 
802
  output=_truncate(
803
  {
804
  "reward_total": reward,
805
+ "docker_reward": report.get("docker_reward"),
806
+ "shaped": report.get("shaped"),
807
  "pass_frac": report.get("pass_frac"),
808
  "resolved": report.get("resolved"),
809
  "resolution": report.get("resolution"),
 
823
  lf.score_current_trace(
824
  name="n_tool_calls", value=float(len(self._tool_calls))
825
  )
826
+ if isinstance(report.get("shaped"), dict):
827
+ for sk, sv in report["shaped"].items():
828
+ lf.score_current_trace(name=f"shaped_{sk}", value=float(sv))
829
  except Exception:
830
  pass
831
  if self._root is not None:
 
836
  "n_tool_calls": len(self._tool_calls),
837
  "tools_used": [c["name"] for c in self._tool_calls],
838
  "resolution": report.get("resolution"),
839
+ "shaped": report.get("shaped"),
840
  }
841
  )
842
  except Exception:
 
855
  resolved=report.get("resolved"),
856
  error=report.get("error"),
857
  n_tool_calls=len(self._tool_calls),
858
+ shaped=report.get("shaped"),
859
  )
860
 
861
  # W&B: one point per completed rollout (train + ckpt eval)
 
864
 
865
  if wandb.run is not None:
866
  tools_used = [c.get("name") for c in self._tool_calls]
867
+ shaped = report.get("shaped") if isinstance(report.get("shaped"), dict) else {}
868
  wandb.log(
869
  {
870
  "rollout/reward_total": float(reward),
871
+ "rollout/docker_reward": float(report.get("docker_reward") or 0.0),
872
  "rollout/pass_frac": float(report.get("pass_frac") or 0.0),
873
  "rollout/resolved": 1.0 if report.get("resolved") else 0.0,
874
  "rollout/n_tool_calls": float(len(self._tool_calls)),
 
876
  "rollout/tools_used": ",".join(tools_used),
877
  "rollout/resolution": str(report.get("resolution") or ""),
878
  "rollout/grader_error": str(report.get("error") or ""),
879
+ "rollout/shaped_tool_bonus": float(shaped.get("tool_bonus") or 0.0),
880
+ "rollout/shaped_edit_bonus": float(shaped.get("edit_bonus") or 0.0),
881
+ "rollout/shaped_patch_bonus": float(shaped.get("patch_bonus") or 0.0),
882
  }
883
  )
884
  _vlog("env.get_reward.wandb_logged")