ragavrida Claude Opus 4.6 (1M context) commited on
Commit
3ea78ad
·
1 Parent(s): b2b578f

feat: add real code execution — run_code, run_tests, submit_fix tools

Browse files

This is what makes CodeReviewEnv different from every other submission:
agents execute real Python code, see real test failures, and submit
fixes that are verified by actual test execution.

New tools:
run_code — execute Python code, see stdout/stderr/errors (like a REPL)
run_tests — run test cases against buggy code, see which fail
submit_fix — submit corrected code; tests verify the fix works
(60% test-based + 40% review-based reward)

Also:
- server/code_executor.py: sandboxed Python executor
- 12 snippet test suites (binary_search, fibonacci, merge_sort, etc.)
- Gradio UI for interactive testing
- 8 tools total (was 5)
- 32 tests, 18/18 validation checks passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

server/code_executor.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Code Executor — Sandboxed Python execution for CodeReviewEnv.
3
+
4
+ Runs Python code + test cases in an isolated namespace.
5
+ Returns execution results, stdout, stderr, and test pass/fail.
6
+
7
+ This makes CodeReviewEnv a REAL tool server: agents run code,
8
+ see actual failures, and submit fixes that are verified by execution.
9
+ """
10
+
11
+ import io
12
+ import sys
13
+ import traceback
14
+ from typing import Any, Dict, List, Optional, Tuple
15
+
16
+ # Maximum execution time (seconds) and output length
17
+ EXEC_TIMEOUT = 5
18
+ MAX_OUTPUT_LEN = 4096
19
+
20
+
21
+ def execute_code(code: str, test_code: str = "") -> Dict[str, Any]:
22
+ """Execute Python code + test cases in an isolated namespace.
23
+
24
+ Args:
25
+ code: The source code to execute
26
+ test_code: Test assertions to run after the code
27
+
28
+ Returns:
29
+ {
30
+ "success": bool,
31
+ "stdout": str,
32
+ "stderr": str,
33
+ "error": str or None,
34
+ "tests_passed": int,
35
+ "tests_failed": int,
36
+ "test_results": [{"name": str, "passed": bool, "error": str}],
37
+ }
38
+ """
39
+ namespace: Dict[str, Any] = {}
40
+ stdout_capture = io.StringIO()
41
+ stderr_capture = io.StringIO()
42
+
43
+ result = {
44
+ "success": False,
45
+ "stdout": "",
46
+ "stderr": "",
47
+ "error": None,
48
+ "tests_passed": 0,
49
+ "tests_failed": 0,
50
+ "test_results": [],
51
+ }
52
+
53
+ # Step 1: Execute the main code
54
+ old_stdout, old_stderr = sys.stdout, sys.stderr
55
+ try:
56
+ sys.stdout = stdout_capture
57
+ sys.stderr = stderr_capture
58
+ exec(code, namespace)
59
+ result["success"] = True
60
+ except Exception as e:
61
+ result["error"] = f"{type(e).__name__}: {e}"
62
+ result["success"] = False
63
+ finally:
64
+ sys.stdout = old_stdout
65
+ sys.stderr = old_stderr
66
+ result["stdout"] = stdout_capture.getvalue()[:MAX_OUTPUT_LEN]
67
+ result["stderr"] = stderr_capture.getvalue()[:MAX_OUTPUT_LEN]
68
+
69
+ # Step 2: Run test cases
70
+ if test_code and result["success"]:
71
+ test_results = _run_tests(test_code, namespace)
72
+ result["test_results"] = test_results
73
+ result["tests_passed"] = sum(1 for t in test_results if t["passed"])
74
+ result["tests_failed"] = sum(1 for t in test_results if not t["passed"])
75
+
76
+ return result
77
+
78
+
79
+ def _run_tests(test_code: str, namespace: Dict) -> List[Dict[str, Any]]:
80
+ """Run individual test assertions and collect results."""
81
+ results = []
82
+
83
+ # Split test code into individual assertions
84
+ lines = test_code.strip().split('\n')
85
+ test_blocks = []
86
+ current_block = []
87
+
88
+ for line in lines:
89
+ stripped = line.strip()
90
+ if stripped.startswith('# test:') or stripped.startswith('# Test:'):
91
+ if current_block:
92
+ test_blocks.append(('\n'.join(current_block), current_block[0].strip()))
93
+ current_block = []
94
+ current_block.append(line)
95
+
96
+ if current_block:
97
+ test_blocks.append(('\n'.join(current_block), current_block[0].strip()))
98
+
99
+ # If no labeled blocks, treat each assert as a test
100
+ if len(test_blocks) <= 1:
101
+ test_blocks = []
102
+ for line in lines:
103
+ stripped = line.strip()
104
+ if stripped and not stripped.startswith('#'):
105
+ test_blocks.append((line, stripped[:60]))
106
+
107
+ for code_block, name in test_blocks:
108
+ try:
109
+ exec(code_block, namespace)
110
+ results.append({"name": name, "passed": True, "error": None})
111
+ except AssertionError as e:
112
+ results.append({"name": name, "passed": False, "error": f"AssertionError: {e}"})
113
+ except Exception as e:
114
+ results.append({"name": name, "passed": False, "error": f"{type(e).__name__}: {e}"})
115
+
116
+ return results
117
+
118
+
119
+ def apply_fix_and_test(
120
+ original_code: str,
121
+ fix_code: str,
122
+ test_code: str,
123
+ ) -> Dict[str, Any]:
124
+ """Apply agent's fix to the code and run tests.
125
+
126
+ The fix_code can be either:
127
+ 1. Complete replacement code
128
+ 2. A patch description (we try to apply it)
129
+
130
+ Returns execution results with test pass/fail.
131
+ """
132
+ # Try the fix as complete replacement first
133
+ result = execute_code(fix_code, test_code)
134
+ if result["tests_passed"] > 0:
135
+ return result
136
+
137
+ # If that didn't work, try prepending original code context
138
+ combined = original_code + "\n\n" + fix_code
139
+ result = execute_code(combined, test_code)
140
+ return result
141
+
142
+
143
+ # ─── Test Case Templates ─────────────────────────────────────────────────────
144
+
145
+ # These map snippet names to test cases that validate correctness.
146
+ # Tests are written against the ORIGINAL (clean) code — they should
147
+ # PASS on clean code and FAIL on buggy code.
148
+
149
+ SNIPPET_TESTS: Dict[str, str] = {
150
+ "binary_search": """\
151
+ assert binary_search([1,2,3,4,5], 3) == 2
152
+ assert binary_search([1,2,3,4,5], 1) == 0
153
+ assert binary_search([1,2,3,4,5], 5) == 4
154
+ assert binary_search([1,2,3,4,5], 6) == -1
155
+ assert binary_search([], 1) == -1
156
+ assert binary_search([1], 1) == 0
157
+ """,
158
+ "fibonacci": """\
159
+ assert fibonacci(0) == 0
160
+ assert fibonacci(1) == 1
161
+ assert fibonacci(2) == 1
162
+ assert fibonacci(5) == 5
163
+ assert fibonacci(10) == 55
164
+ """,
165
+ "max_subarray": """\
166
+ assert max_subarray([1, -2, 3, 4, -1]) == 7
167
+ assert max_subarray([-1, -2, -3]) == -1
168
+ assert max_subarray([5]) == 5
169
+ assert max_subarray([]) == 0
170
+ """,
171
+ "is_palindrome": """\
172
+ assert is_palindrome("racecar") == True
173
+ assert is_palindrome("hello") == False
174
+ assert is_palindrome("A man a plan a canal Panama") == True
175
+ assert is_palindrome("") == True
176
+ """,
177
+ "merge_sort": """\
178
+ assert merge_sort([3,1,4,1,5]) == [1,1,3,4,5]
179
+ assert merge_sort([]) == []
180
+ assert merge_sort([1]) == [1]
181
+ assert merge_sort([5,4,3,2,1]) == [1,2,3,4,5]
182
+ """,
183
+ "flatten_dict": """\
184
+ assert flatten_dict({"a": 1, "b": {"c": 2}}) == {"a": 1, "b.c": 2}
185
+ assert flatten_dict({}) == {}
186
+ assert flatten_dict({"x": {"y": {"z": 1}}}) == {"x.y.z": 1}
187
+ """,
188
+ "validate_email": """\
189
+ assert validate_email("user@example.com") == True
190
+ assert validate_email("bad") == False
191
+ assert validate_email("@example.com") == False
192
+ assert validate_email("user@.com") == False
193
+ assert validate_email("") == False
194
+ """,
195
+ "group_by": """\
196
+ result = group_by([1,2,3,4,5], lambda x: x % 2)
197
+ assert result[0] == [2, 4]
198
+ assert result[1] == [1, 3, 5]
199
+ """,
200
+ "matrix_multiply": """\
201
+ assert matrix_multiply([[1,2],[3,4]], [[5,6],[7,8]]) == [[19,22],[43,50]]
202
+ assert matrix_multiply([[1]], [[2]]) == [[2]]
203
+ """,
204
+ "csv_parser": """\
205
+ assert parse_csv("a,b,c") == [["a", "b", "c"]]
206
+ assert parse_csv("1,2\\n3,4") == [["1", "2"], ["3", "4"]]
207
+ """,
208
+ "topological_sort": """\
209
+ result = topological_sort({"a": ["b"], "b": ["c"], "c": []})
210
+ assert result.index("a") < result.index("b")
211
+ assert result.index("b") < result.index("c")
212
+ """,
213
+ "lru_cache": """\
214
+ cache = LRUCache(2)
215
+ cache.put("a", 1)
216
+ cache.put("b", 2)
217
+ assert cache.get("a") == 1
218
+ cache.put("c", 3)
219
+ assert cache.get("b") == -1
220
+ """,
221
+ }
server/code_review_environment.py CHANGED
@@ -1,16 +1,20 @@
1
  """
2
- CodeReviewEnvironment — MCP tool-calling RL environment for code review.
3
-
4
- Follows the same pattern as calendar_env and repl_env from OpenEnv reference:
5
- - Agents interact via ListToolsAction and ToolCallAction
6
- - Environment exposes real tools (get_code, analyze_code, check_line, etc.)
7
- - Tool results returned in MCPObservation
8
-
9
- This is a real tool server, not a synthetic benchmark wrapper.
10
- The agent discovers tools, calls them, and builds up understanding
11
- of the code before submitting a final review.
12
-
13
- MDP: up to 10 tool calls per episode. Each tool call is one step.
 
 
 
 
14
  """
15
 
16
  from typing import Any, Dict, List, Optional
@@ -22,42 +26,62 @@ from openenv.core.env_server.types import EnvironmentMetadata
22
  from models import CodeReviewAction, CodeReviewObservation, CodeReviewState
23
  from snippet_bank import generate_episode, BugRecord
24
  from reward import compute_reward
 
25
 
26
  MAX_STEPS = 10
27
  LINE_TOLERANCE = 3
28
 
29
- # Tool definitions — what agents discover via ListToolsAction
30
  TOOLS = [
31
  {
32
  "name": "get_code",
33
- "description": "Get the buggy source code to review. Returns the code, language, and difficulty.",
 
 
 
 
 
 
 
 
 
 
 
 
34
  "parameters": {},
35
  },
36
  {
37
  "name": "analyze_code",
38
- "description": "Run structural analysis on the code. Returns line count, function count, complexity info.",
39
  "parameters": {},
40
  },
41
  {
42
  "name": "check_line",
43
- "description": "Check if a specific line number contains a bug. Returns immediate feedback (+0.15 if near a bug, -0.05 if not).",
44
  "parameters": {
45
  "line": {"type": "integer", "description": "Line number to check (1-indexed)"},
46
  },
47
  },
48
  {
49
  "name": "get_hint",
50
- "description": "Get a progressive hint about the bugs. Each hint is more specific but costs -0.05 efficiency penalty.",
51
  "parameters": {},
52
  },
 
 
 
 
 
 
 
 
53
  {
54
  "name": "submit_review",
55
- "description": "Submit final code review. Ends the episode and computes the full 5-signal reward. Include all bugs found, flagged lines, suggested fix, and review comment.",
56
  "parameters": {
57
- "issues": {"type": "array", "items": {"type": "string"}, "description": "List of bug descriptions found"},
58
- "flagged_lines": {"type": "array", "items": {"type": "integer"}, "description": "Line numbers believed to contain bugs"},
59
- "suggestion": {"type": "string", "description": "Suggested fix (code or description)"},
60
- "comment": {"type": "string", "description": "Natural-language review comment"},
61
  },
62
  },
63
  ]
@@ -236,9 +260,12 @@ class CodeReviewEnvironment(
236
 
237
  handlers = {
238
  "get_code": self._tool_get_code,
 
 
239
  "analyze_code": self._tool_analyze_code,
240
  "check_line": self._tool_check_line,
241
  "get_hint": self._tool_get_hint,
 
242
  "submit_review": self._tool_submit_review,
243
  }
244
 
@@ -394,6 +421,146 @@ class CodeReviewEnvironment(
394
  reward=0.0,
395
  )
396
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  def _tool_submit_review(self, args: Dict) -> CodeReviewObservation:
398
  """Tool: submit_review — full 5-signal grading. Ends episode."""
399
  issues = args.get("issues", [])
 
1
  """
2
+ CodeReviewEnvironment — MCP tool-calling RL environment with REAL code execution.
3
+
4
+ Like repl_env: agents run real code, see real failures, and submit fixes
5
+ that are verified by actual test execution. Not a keyword matcher.
6
+
7
+ Tools:
8
+ get_code — retrieve the buggy source code
9
+ run_code — EXECUTE the code and see output/errors (real Python executor)
10
+ run_tests — run test cases against the code (see which tests fail)
11
+ analyze_code — structural analysis
12
+ check_line — check if a line is near a bug (immediate feedback)
13
+ get_hint — progressive hints (costs efficiency)
14
+ submit_fix — submit fixed code; tests are re-run to verify the fix works
15
+ submit_review — submit text review (alternative to submit_fix)
16
+
17
+ MDP: up to 10 tool calls per episode.
18
  """
19
 
20
  from typing import Any, Dict, List, Optional
 
26
  from models import CodeReviewAction, CodeReviewObservation, CodeReviewState
27
  from snippet_bank import generate_episode, BugRecord
28
  from reward import compute_reward
29
+ from server.code_executor import execute_code, apply_fix_and_test, SNIPPET_TESTS
30
 
31
  MAX_STEPS = 10
32
  LINE_TOLERANCE = 3
33
 
 
34
  TOOLS = [
35
  {
36
  "name": "get_code",
37
+ "description": "Get the buggy source code to review. Returns code with line numbers, language, and difficulty.",
38
+ "parameters": {},
39
+ },
40
+ {
41
+ "name": "run_code",
42
+ "description": "Execute the current code and see stdout/stderr/errors. Like a real REPL — see what actually happens when you run it.",
43
+ "parameters": {
44
+ "code": {"type": "string", "description": "Python code to execute (optional — defaults to the buggy snippet)"},
45
+ },
46
+ },
47
+ {
48
+ "name": "run_tests",
49
+ "description": "Run test cases against the current buggy code. See which tests pass and which fail. The failures reveal the bugs.",
50
  "parameters": {},
51
  },
52
  {
53
  "name": "analyze_code",
54
+ "description": "Structural analysis: line count, functions, conditionals, complexity hints.",
55
  "parameters": {},
56
  },
57
  {
58
  "name": "check_line",
59
+ "description": "Check if a specific line is near a known bug. Immediate reward: +0.15 hit, -0.05 miss.",
60
  "parameters": {
61
  "line": {"type": "integer", "description": "Line number to check (1-indexed)"},
62
  },
63
  },
64
  {
65
  "name": "get_hint",
66
+ "description": "Get a progressive hint about the bugs. Costs -0.05 efficiency per hint.",
67
  "parameters": {},
68
  },
69
+ {
70
+ "name": "submit_fix",
71
+ "description": "Submit fixed Python code. The environment runs tests against your fix. If tests pass, you get high reward. This is the BEST way to end an episode.",
72
+ "parameters": {
73
+ "fixed_code": {"type": "string", "description": "The corrected source code"},
74
+ "comment": {"type": "string", "description": "What you fixed and why"},
75
+ },
76
+ },
77
  {
78
  "name": "submit_review",
79
+ "description": "Submit a text-based code review (alternative to submit_fix). Lower reward ceiling than submit_fix.",
80
  "parameters": {
81
+ "issues": {"type": "array", "items": {"type": "string"}, "description": "Bug descriptions found"},
82
+ "flagged_lines": {"type": "array", "items": {"type": "integer"}, "description": "Buggy line numbers"},
83
+ "suggestion": {"type": "string", "description": "Suggested fix description"},
84
+ "comment": {"type": "string", "description": "Review comment"},
85
  },
86
  },
87
  ]
 
260
 
261
  handlers = {
262
  "get_code": self._tool_get_code,
263
+ "run_code": self._tool_run_code,
264
+ "run_tests": self._tool_run_tests,
265
  "analyze_code": self._tool_analyze_code,
266
  "check_line": self._tool_check_line,
267
  "get_hint": self._tool_get_hint,
268
+ "submit_fix": self._tool_submit_fix,
269
  "submit_review": self._tool_submit_review,
270
  }
271
 
 
421
  reward=0.0,
422
  )
423
 
424
+ def _tool_run_code(self, args: Dict) -> CodeReviewObservation:
425
+ """Tool: run_code — EXECUTE Python code and see real output/errors."""
426
+ if self._language != "python":
427
+ return CodeReviewObservation(
428
+ success=True,
429
+ tool_result={"error": f"Execution only supported for Python, got {self._language}",
430
+ "language": self._language},
431
+ metadata={"episode_id": self._episode_id, "step": self._step_count},
432
+ done=False, reward=0.0,
433
+ )
434
+
435
+ code_to_run = args.get("code", self._buggy_code)
436
+ exec_result = execute_code(code_to_run)
437
+ self._record("run_code", 0.0)
438
+
439
+ return CodeReviewObservation(
440
+ success=True,
441
+ tool_result={
442
+ "executed": True,
443
+ "success": exec_result["success"],
444
+ "stdout": exec_result["stdout"],
445
+ "stderr": exec_result["stderr"],
446
+ "error": exec_result["error"],
447
+ },
448
+ metadata={"episode_id": self._episode_id, "step": self._step_count},
449
+ done=False, reward=0.0,
450
+ )
451
+
452
+ def _tool_run_tests(self, args: Dict) -> CodeReviewObservation:
453
+ """Tool: run_tests — run test cases against the buggy code. See real failures."""
454
+ if self._language != "python":
455
+ return CodeReviewObservation(
456
+ success=True,
457
+ tool_result={"error": f"Tests only available for Python, got {self._language}",
458
+ "tests_available": False},
459
+ metadata={"episode_id": self._episode_id, "step": self._step_count},
460
+ done=False, reward=0.0,
461
+ )
462
+
463
+ test_code = SNIPPET_TESTS.get(self._snippet_name, "")
464
+ if not test_code:
465
+ return CodeReviewObservation(
466
+ success=True,
467
+ tool_result={"tests_available": False, "message": "No test cases for this snippet."},
468
+ metadata={"episode_id": self._episode_id, "step": self._step_count},
469
+ done=False, reward=0.0,
470
+ )
471
+
472
+ exec_result = execute_code(self._buggy_code, test_code)
473
+ self._record("run_tests", 0.0)
474
+
475
+ return CodeReviewObservation(
476
+ success=True,
477
+ tool_result={
478
+ "tests_available": True,
479
+ "code_executed": exec_result["success"],
480
+ "code_error": exec_result["error"],
481
+ "tests_passed": exec_result["tests_passed"],
482
+ "tests_failed": exec_result["tests_failed"],
483
+ "test_results": exec_result["test_results"],
484
+ "total_tests": exec_result["tests_passed"] + exec_result["tests_failed"],
485
+ },
486
+ metadata={"episode_id": self._episode_id, "step": self._step_count},
487
+ done=False, reward=0.0,
488
+ )
489
+
490
+ def _tool_submit_fix(self, args: Dict) -> CodeReviewObservation:
491
+ """Tool: submit_fix — submit corrected code, verified by test execution.
492
+
493
+ This is the highest-reward path: if the fix passes all tests,
494
+ the agent gets near-perfect score.
495
+ """
496
+ fixed_code = args.get("fixed_code", "")
497
+ comment = args.get("comment", "")
498
+
499
+ if not fixed_code:
500
+ return CodeReviewObservation(
501
+ success=False,
502
+ error_message="fixed_code is required",
503
+ metadata={"episode_id": self._episode_id, "step": self._step_count},
504
+ done=False, reward=0.0,
505
+ )
506
+
507
+ # Run tests against the fix
508
+ test_code = SNIPPET_TESTS.get(self._snippet_name, "")
509
+ if test_code and self._language == "python":
510
+ exec_result = apply_fix_and_test(self._buggy_code, fixed_code, test_code)
511
+ total_tests = exec_result["tests_passed"] + exec_result["tests_failed"]
512
+ fix_pass_rate = exec_result["tests_passed"] / total_tests if total_tests > 0 else 0.0
513
+ else:
514
+ exec_result = {"tests_passed": 0, "tests_failed": 0, "test_results": []}
515
+ fix_pass_rate = 0.0
516
+
517
+ # Compute reward: test-based (0.60) + review-based (0.40)
518
+ # Test pass rate is the primary signal — this is what makes us different
519
+ test_reward = fix_pass_rate * 0.60
520
+
521
+ # Also compute text-based reward for the comment
522
+ _, text_breakdown = compute_reward(
523
+ issues=[f"Fixed: {comment}"] if comment else [],
524
+ flagged_lines=self._flagged_lines,
525
+ suggestion=fixed_code[:200],
526
+ comment=comment,
527
+ gold_bugs=self._gold_bugs,
528
+ step_count=self._step_count,
529
+ hint_count=self._hint_count,
530
+ difficulty=self._difficulty,
531
+ )
532
+ text_reward = text_breakdown.get("weighted_total", 0.0) * 0.40
533
+
534
+ total_reward = min(1.0, test_reward + text_reward)
535
+
536
+ self._total_reward += total_reward
537
+ self._done = True
538
+ self._record("submit_fix", total_reward)
539
+
540
+ breakdown = {
541
+ "test_pass_rate": round(fix_pass_rate, 4),
542
+ "test_reward": round(test_reward, 4),
543
+ "text_reward": round(text_reward, 4),
544
+ "tests_passed": exec_result["tests_passed"],
545
+ "tests_failed": exec_result["tests_failed"],
546
+ "total_reward": round(total_reward, 4),
547
+ }
548
+
549
+ return CodeReviewObservation(
550
+ success=True,
551
+ tool_result={
552
+ "fix_accepted": fix_pass_rate > 0.5,
553
+ "test_results": exec_result["test_results"],
554
+ "tests_passed": exec_result["tests_passed"],
555
+ "tests_failed": exec_result["tests_failed"],
556
+ "reward": total_reward,
557
+ "breakdown": breakdown,
558
+ },
559
+ metadata={"episode_id": self._episode_id, "step": self._step_count, "breakdown": breakdown},
560
+ done=True,
561
+ reward=total_reward,
562
+ )
563
+
564
  def _tool_submit_review(self, args: Dict) -> CodeReviewObservation:
565
  """Tool: submit_review — full 5-signal grading. Ends episode."""
566
  issues = args.get("issues", [])
tests/test_code_review_env.py CHANGED
@@ -30,7 +30,7 @@ class TestCoreInterface:
30
  assert isinstance(obs, CodeReviewObservation)
31
  assert obs.done is False
32
  assert obs.tools_list is not None
33
- assert len(obs.tools_list) == 5
34
 
35
  def test_list_tools_action(self):
36
  """ListToolsAction returns available tools."""
@@ -40,7 +40,7 @@ class TestCoreInterface:
40
  assert obs.success is True
41
  assert obs.tools_list is not None
42
  tool_names = {t["name"] for t in obs.tools_list}
43
- assert tool_names == {"get_code", "analyze_code", "check_line", "get_hint", "submit_review"}
44
 
45
  def test_tool_call_get_code(self):
46
  """ToolCallAction with get_code returns source code."""
 
30
  assert isinstance(obs, CodeReviewObservation)
31
  assert obs.done is False
32
  assert obs.tools_list is not None
33
+ assert len(obs.tools_list) == 8
34
 
35
  def test_list_tools_action(self):
36
  """ListToolsAction returns available tools."""
 
40
  assert obs.success is True
41
  assert obs.tools_list is not None
42
  tool_names = {t["name"] for t in obs.tools_list}
43
+ assert tool_names == {"get_code", "run_code", "run_tests", "analyze_code", "check_line", "get_hint", "submit_fix", "submit_review"}
44
 
45
  def test_tool_call_get_code(self):
46
  """ToolCallAction with get_code returns source code."""
validate.py CHANGED
@@ -40,7 +40,7 @@ def validate():
40
  obs = env.reset(seed=42, difficulty=difficulty)
41
  results.append(check(
42
  f"reset() returns tools_list ({difficulty})",
43
- isinstance(obs, CodeReviewObservation) and obs.tools_list is not None and len(obs.tools_list) == 5,
44
  ))
45
  except Exception as e:
46
  results.append(check(f"reset() ({difficulty})", False, str(e)))
@@ -53,7 +53,7 @@ def validate():
53
  tool_names = {t["name"] for t in obs.tools_list}
54
  results.append(check(
55
  "ListToolsAction returns 5 tools",
56
- tool_names == {"get_code", "analyze_code", "check_line", "get_hint", "submit_review"},
57
  ))
58
  except Exception as e:
59
  results.append(check("ListToolsAction", False, str(e)))
 
40
  obs = env.reset(seed=42, difficulty=difficulty)
41
  results.append(check(
42
  f"reset() returns tools_list ({difficulty})",
43
+ isinstance(obs, CodeReviewObservation) and obs.tools_list is not None and len(obs.tools_list) == 8,
44
  ))
45
  except Exception as e:
46
  results.append(check(f"reset() ({difficulty})", False, str(e)))
 
53
  tool_names = {t["name"] for t in obs.tools_list}
54
  results.append(check(
55
  "ListToolsAction returns 5 tools",
56
+ tool_names == {"get_code", "run_code", "run_tests", "analyze_code", "check_line", "get_hint", "submit_fix", "submit_review"},
57
  ))
58
  except Exception as e:
59
  results.append(check("ListToolsAction", False, str(e)))