Skip to content

[Recipe] add Claude Code recipe agent loop - #1695

Merged
hhaAndroid merged 1 commit into
InternLM:rl_designfrom
YanhuiDua:add_cc_recipe
Apr 22, 2026
Merged

hhaAndroid merged 1 commit into
InternLM:rl_designfrom
YanhuiDua:add_cc_recipe

Conversation

@YanhuiDua

Copy link
Copy Markdown
Collaborator

No description provided.

@YanhuiDua
YanhuiDua force-pushed the add_cc_recipe branch 2 times, most recently from 2b1519e to eb229a6 Compare April 21, 2026 14:29
@YanhuiDua

Copy link
Copy Markdown
Collaborator Author

@claude review

kwargs = {
"return_token_ids": True,
"return_logprob": False,
"return_logprob": True,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: Warning — Unconditional return_logprob=True is a behavioral change for all callers.

This changes from False to True for every request going through LocalRolloutBackend, not just Claude Code agent loops. Returning logprobs requires additional GPU memory and computation. If the only consumer that needs logprobs is the new ClaudeCodeAgentLoop (which reads them in chat_trace_records_to_rollout_states), consider making this request-driven instead — e.g., respect a flag on the canonical request or the SampleParams, so existing callers aren't impacted.

Suggested change
"return_logprob": True,
"return_logprob": canonical_request.return_logprob if hasattr(canonical_request, "return_logprob") else True,

If all callers on rl_design actually do need logprobs, please add a brief comment explaining why the default was flipped.

Comment on lines +7 to +8
XUTNER_DIR="${XUTNER_DIR:-${REPO_ROOT}}"
XTUNER_DIR="${XTUNER_DIR:-${XUTNER_DIR}}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: Warning — Typo XUTNER_DIR is confusing and fragile.

Lines 7-8 define XUTNER_DIR (typo) then use it as the fallback for XTUNER_DIR. Later, PYTHONPATH (line 33) is set from XUTNER_DIR (the typo'd version) and both are exported. While the fallback chain makes this functionally equivalent to using XTUNER_DIR directly, anyone reading this script will be confused about whether XUTNER_DIR is an intentional legacy env var or a typo.

If it's not an intentional backwards-compat alias, consider simplifying to:

XTUNER_DIR="${XTUNER_DIR:-${REPO_ROOT}}"

and updating the PYTHONPATH and export lines accordingly.

Comment on lines 592 to +620
blocks.append({"type": "thinking", "thinking": reasoning_text})
return blocks or [{"type": "text", "text": ""}]

def _sanitize_tool_call_for_request(
self,
tool_call: CanonicalToolCall,
*,
tools: list[CanonicalToolDefinition],
) -> CanonicalToolCall:
tool_definition = next((tool for tool in tools if tool.name == tool_call.name), None)
if tool_definition is None:
return tool_call

properties = tool_definition.parameters_json_schema.get("properties")
if not isinstance(properties, dict):
return tool_call

arguments = tool_call.arguments
normalized_arguments = False
if not isinstance(arguments, dict):
normalized_arguments = True
if tool_call.raw_arguments_text is not None:
try:
decoded = json.loads(tool_call.raw_arguments_text)
except Exception:
decoded = {"raw": tool_call.raw_arguments_text}
arguments = decoded if isinstance(decoded, dict) else {"value": decoded}
elif arguments is None:
arguments = {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: Nit — Consider logging when arguments are dropped.

The sanitization logic correctly strips unknown keys and records them in metadata["dropped_arguments"], which is great for post-hoc analysis. However, silently dropping tool call arguments in a training pipeline could be hard to debug when things go wrong.

Consider adding a logger.debug(...) or logger.warning(...) when dropped_keys is non-empty, so operators can see this happening in real time:

if dropped_keys:
    metadata["dropped_arguments"] = dropped_keys
    logger.debug("Dropped unknown arguments %s from tool call %s", dropped_keys, tool_call.name)

Comment on lines +88 to +93
class ClaudeCodeAgentLoop(AgentLoop):
def __init__(
self,
claude_command: list[str],
cwd: str | None,
timeout_s: float,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: Nit — Missing type annotations on build_local parameters.

Per project standards in CLAUDE.md: "All new code must include type hints for function signatures." The rollout_controller and logger parameters lack type annotations.

Suggested change
class ClaudeCodeAgentLoop(AgentLoop):
def __init__(
self,
claude_command: list[str],
cwd: str | None,
timeout_s: float,
def build_local(
self,
rollout_controller: RolloutController,
judger: Judger | None = None,
logger: Any = None,
) -> ClaudeCodeAgentLoop:

self._failed_state(
rollout_state,
"Judger completed without setting reward.",
extra_fields=rollout_extra_fields,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: Nit — api_key used for both auth and trace store correlation is a security anti-pattern.

The api_key is a random UUID hex used simultaneously as: (1) the ANTHROPIC_API_KEY / Authorization: Bearer token, and (2) the key to look up trace store records via /trace_store/pop. This means the trace store endpoint effectively becomes an unauthenticated data exfiltration path — anyone who can guess or observe the key can pop another session's training data.

In the current setup (localhost gateway, single-machine), this is low risk. But if the gateway is ever exposed to a network, consider separating the auth token from the trace correlation ID.

Comment on lines +36 to +38
</parameter>
</function>
</tool_call>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: Nit — CALCULATOR_PROMPT embeds model-specific XML syntax.

The prompt hardcodes Qwen-style <tool_call>/<function=...> XML syntax, which is specific to the Qwen tool-calling format. If this recipe is meant to work with other models, this would break. If it's intentionally Qwen-only, a comment noting that would help future readers.

Same applies to CALCULATOR_SYSTEM_PROMPT below (line 56).

@claude

claude Bot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Claude: ## Summary

Adds a ClaudeCodeAgentLoop that shells out to the Claude Code CLI for RL rollout collection, plus a calculator-based E2E test recipe. The core library changes plumb rollout_config directly through the gateway stack (avoiding an extra Ray RPC), flip return_logprob to True, and add tool-call argument sanitization in the Anthropic adapter. A new chat_trace_records_to_rollout_states utility converts gateway trace records into trainable RolloutState objects.

Issues

Warning

  • local_backend.py:301 — return_logprob changed from False to True unconditionally. This affects all LocalRolloutBackend users, not just the Claude Code agent loop. Consider making it request-driven.
  • run_claudecode_tool_e2e.sh:7-8 — XUTNER_DIR appears to be a typo for XTUNER_DIR. The fallback chain makes it functionally correct, but the naming is confusing.

Nit

Verdict

COMMENT — The recipe and utility code are well-structured. The main concern is the unconditional return_logprob=True change in local_backend.py, which broadens beyond this PR's scope. The rest are minor style/observability suggestions.

@YanhuiDua
YanhuiDua force-pushed the add_cc_recipe branch 2 times, most recently from d0db86d to 9e05dd1 Compare April 22, 2026 04:51
@YanhuiDua

Copy link
Copy Markdown
Collaborator Author

@claude review, if you think this pr is ready, give it an approve

Comment on lines +203 to +204
except Exception as exc:
return [self._failed_state(rollout_state, f"ClaudeCodeAgentLoop failed: {exc}")]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: Warning — Broad except Exception swallows programming bugs.

This catch-all turns every error (including KeyError, AttributeError, TypeError from coding bugs) into a FAILED rollout state with only a string message. During development this makes debugging extremely difficult — a typo in a field name silently becomes "ClaudeCodeAgentLoop failed: 'foo'" instead of a traceback.

Consider narrowing to expected failure modes (e.g., httpx.HTTPError, asyncio.TimeoutError, RuntimeError) and letting unexpected exceptions propagate, or at minimum logging the full traceback before converting to a failed state:

except Exception as exc:
    logger.exception("ClaudeCodeAgentLoop failed unexpectedly")
    return [self._failed_state(rollout_state, f"ClaudeCodeAgentLoop failed: {exc}")]

last_error = repr(exc)
time.sleep(1.0)
raise AssertionError(f"Gateway did not become ready at {base_url}: {last_error}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: Nit — AssertionError is semantically wrong for a runtime timeout.

AssertionError conventionally signals a violated program invariant (and is suppressed by python -O). A gateway failing to become ready within a timeout is a runtime condition, not an assertion. Consider TimeoutError or RuntimeError instead:

Suggested change
raise TimeoutError(f"Gateway did not become ready at {base_url}: {last_error}")

Comment on lines +177 to +180
if bound_ports is not None:
return bound_ports
else:
assert end_port is not None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude: Nit — Contiguous port search uses fixed 100 random attempts, which is fragile for large nums.

With random.randint(20000, 60000 - nums), the search space is ~40k candidates but only 100 are tried. If the machine has scattered port reservations, this can fail even when a valid contiguous range exists. A sequential scan from a random start point would be more reliable:

start = random.randint(20000, 60000 - nums)
for offset in range(60000 - nums - 20000):
    candidate = 20000 + (start - 20000 + offset) % (60000 - nums - 20000)
    bound_ports = try_bind_ports(list(range(candidate, candidate + nums)))
    if bound_ports is not None:
        return bound_ports

Not critical since 100 attempts is usually sufficient in practice, but worth noting.

@claude

claude Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Claude: ## Review (round 2)

The PR was force-pushed since the last review but the substance is unchanged — none of the previous inline comments were addressed.

Summary

Adds a ClaudeCodeAgentLoop that shells out to the Claude Code CLI for RL rollout collection, a calculator-tool E2E recipe, and supporting library changes: rollout_config passthrough to avoid extra Ray RPCs, tool-call argument sanitization in the Anthropic adapter, find_free_ports consolidation, and chat_trace_records_to_rollout_states for converting gateway traces to trainable states.

Outstanding issues from the previous review (not yet addressed)

  1. return_logprob=True unconditional (local_backend.py:301) — this affects all LocalRolloutBackend callers, not just Claude Code. It should be request-driven or at minimum documented.
  2. Silent argument dropping in _sanitize_tool_call_for_request — needs logging.
  3. Missing type annotations on build_local parameters — CLAUDE.md requires type hints on all new function signatures.

New findings (this review)

  1. Broad except Exception in generate_sample swallows programming bugs — should either narrow the catch or log the full traceback.
  2. wait_for_gateway_ready raises AssertionError — should be TimeoutError or RuntimeError.
  3. find_free_ports contiguous mode uses 100 random attempts which is fragile for large port ranges.

Verdict

COMMENT — The architecture is solid and the recipe is well-structured, but issue #1 (return_logprob=True for all callers) is a behavioral change that should be addressed or explicitly justified before merging, even into rl_design. The other items are warnings/nits.

@hhaAndroid
hhaAndroid merged commit d28de02 into InternLM:rl_design Apr 22, 2026
2 of 6 checks passed
YanhuiDua added a commit that referenced this pull request Apr 27, 2026
add Claude Code recipe agent loop
hhaAndroid pushed a commit that referenced this pull request Apr 29, 2026
hhaAndroid pushed a commit that referenced this pull request May 15, 2026
@YanhuiDua
YanhuiDua deleted the add_cc_recipe branch July 14, 2026 03:32
@YanhuiDua
YanhuiDua restored the add_cc_recipe branch July 14, 2026 03:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants