Skip to content

feat(deepswe): align OpenHands harness contract with MLPerf v6.1 rules - #2639

Closed
niting wants to merge 1 commit into
atwigg/mlperffrom
niting/openhands-mlperf-v61
Closed

niting wants to merge 1 commit into
atwigg/mlperffrom
niting/openhands-mlperf-v61

Conversation

@niting

@niting niting commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Minimal alignment of the OpenHands (CodeActAgent) scaffold in tunix with the MLPerf Training v6.1 OpenHands agent harness contract (mlcommons/training_policies#599 and mlcommons/training_policies#598), keeping the existing openhands-agent-server:1.44.1 sandbox image and delegating str_replace_editor to the existing RepoEnv file_editor:

  1. Keep openhands-agent-server:1.44.1 (no custom Docker image build):
  2. MLPerf v6.1 Tool Schemas (examples/deepswe/template.py):
    • Exposes all 5 required reference tools in exact reference order (execute_bash, think, finish, task_tracker, str_replace_editor) by setting enable_think: bool = True by default in get_openhands_tools().
    • Removes "security_risk" from "required" in execute_bash and str_replace_editor (while keeping "security_risk" in "properties"), matching the [[qwen35_openhands_agent_harness]] rule in training_rules.adoc.
  3. Prompts & Turn History (examples/deepswe/template.py, examples/deepswe/swe_agent.py):
    • Uses the reference OPENHANDS_CODEACT_SYSTEM_PROMPT (system_prompt.j2 + Qwen3.5 <tools> header) and OPENHANDS_USER_PROMPT (user_prompt.j2 / swe_default.j2).
    • Appends tool observations with role="tool" (<tool_response>), emits OPENHANDS_FAKE_USER_RESPONSE (role="user") when no tool call is made, and omits synthetic Steps Remaining / token-exhaustion prompts in CodeActAgent.
  4. Qwen3.5 XML Tool-Call Parsing (parser.py, swe_agent.py, eval_worker.py):
    • Preserves \n<tool_call>\n and \n</tool_call> in BaseChatTemplateParser.
    • Preserves leading indentation in multiline str_replace_editor old_str/new_str parameters (parse_openhands_xml_action / parse_openhands_action_str).
    • Removes "stop": ["</function>"] for OpenHands scaffolds in eval_worker.py.
  5. Minimal Tool Dispatch & Timeouts (openhands_utils.py, mlperf_base.sh, run_deepswe_dist.py, eval_deepswe.py):
    • Adds think, finish, and task_tracker (view / plan) handlers in step_openhands, and strips "security_risk" before delegating str_replace_editor to the existing env.env.step(action_obj).
    • Aligns default timeouts and turn limits (max_turns=30, step_timeout=60, reward_timeout=60, episode_timeout=1800).

Test Plan

  • PYTHONPATH=. pytest tests/examples/template_test.py examples/deepswe/swe_agent_test.py tests/rl/agentic/parser/chat_template_parser/chat_template_parser_test.py (65 passed)

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request updates the OpenHands agent harness within the tunix framework to ensure full compliance with MLPerf Training v6.1 rules. The changes include standardizing tool schemas, refining prompt templates for Qwen3.5, and implementing local tool execution logic to match the reference harness contract. These updates improve agent reliability and compatibility while maintaining the existing sandbox environment.

Highlights

  • MLPerf v6.1 Alignment: Aligned the OpenHands agent harness with MLPerf Training v6.1 specifications, including updated tool schemas and reference-compliant system prompts.
  • Tool Execution Semantics: Implemented a local OHEditor to match OpenHands semantics and added stateful task_tracker support for better workflow management.
  • Prompt & History Management: Updated system and user prompts to use Qwen3.5 standards and improved turn history tracking to support role-based tool observations.
  • Timeout & Resource Limits: Adjusted default timeouts, turn limits, and response length constraints to align with the updated harness contract.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces comprehensive support for OpenHands scaffolds, including local editor execution, structured task tracking, persistent bash sessions, and Qwen-style XML tool-calling formats. While the implementation is thorough and well-tested, several improvements are needed to align with the repository's style guide. Specifically, broad Exception blocks should be replaced with specific exceptions like OSError and json.JSONDecodeError when handling file operations and JSON parsing. Additionally, speculative hasattr and getattr calls on structured objects like workspace and CommandResult should be replaced with direct attribute access to adhere to the 'One well-lit path' philosophy.

Comment thread examples/deepswe/openhands_utils.py Outdated
Comment on lines +72 to +73
except Exception: # pylint: disable=broad-exception-caught
pass

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.

medium

The repository style guide explicitly advises against catching overly broad exceptions like bare Exception to swallow errors. Since this block is loading history from a JSON file, we should catch specific exceptions such as OSError and json.JSONDecodeError instead.

Suggested change
except Exception: # pylint: disable=broad-exception-caught
pass
except (OSError, json.JSONDecodeError):
pass
References
  1. Use specific exceptions: Avoid catching or raising overly broad exceptions like bare Exception to swallow errors. (link)

Comment thread examples/deepswe/openhands_utils.py Outdated
Comment on lines +81 to +82
except Exception: # pylint: disable=broad-exception-caught
pass

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.

medium

Catching a broad Exception to swallow errors when saving history is discouraged by the style guide. Please catch OSError specifically, as it is the appropriate exception class for file writing and directory creation errors.

Suggested change
except Exception: # pylint: disable=broad-exception-caught
pass
except OSError:
pass
References
  1. Use specific exceptions: Avoid catching or raising overly broad exceptions like bare Exception to swallow errors. (link)

Comment thread examples/deepswe/openhands_utils.py Outdated
if isinstance(raw_range, str):
try:
view_range = json.loads(raw_range)
except Exception: # pylint: disable=broad-exception-caught

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.

medium

Catching a broad Exception when parsing view_range with json.loads is discouraged. Please catch json.JSONDecodeError specifically.

Suggested change
except Exception: # pylint: disable=broad-exception-caught
except json.JSONDecodeError:
References
  1. Use specific exceptions: Avoid catching or raising overly broad exceptions like bare Exception to swallow errors. (link)

Comment thread examples/deepswe/openhands_utils.py Outdated
if isinstance(parsed_commit, str):
try:
parsed_commit = json.loads(parsed_commit)
except Exception:

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.

medium

Catching a broad Exception when parsing parsed_commit with json.loads is discouraged. Please catch json.JSONDecodeError specifically.

Suggested change
except Exception:
except json.JSONDecodeError:
References
  1. Use specific exceptions: Avoid catching or raising overly broad exceptions like bare Exception to swallow errors. (link)

Comment thread examples/deepswe/openhands_utils.py Outdated
if isinstance(raw_list, str):
try:
task_list = json.loads(raw_list)
except Exception as e:

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.

medium

Catching a broad Exception when parsing task_list with json.loads is discouraged. Please catch json.JSONDecodeError specifically.

Suggested change
except Exception as e:
except json.JSONDecodeError as e:
References
  1. Use specific exceptions: Avoid catching or raising overly broad exceptions like bare Exception to swallow errors. (link)

Comment thread examples/deepswe/openhands_utils.py Outdated
Comment on lines +534 to +535
if entry is None and hasattr(workspace, "entry"):
entry = getattr(workspace, "entry", 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.

medium

According to the repository style guide's 'One well-lit path' philosophy, speculative attribute accesses using hasattr and getattr with defaults should be avoided on structured contracts. If workspace is expected to have an entry attribute, we should access it directly or define a strict Protocol/interface rather than using defensive fallbacks.

Suggested change
if entry is None and hasattr(workspace, "entry"):
entry = getattr(workspace, "entry", None)
if entry is None:
entry = workspace.entry
References
  1. One well-lit path (No defensive fallbacks): Never use speculative getattr(obj, 'field', default), dict.get('key', default) on structured schemas... Access attributes and keys directly (obj.field) on a single, strictly typed contract. (link)

Comment thread examples/deepswe/openhands_utils.py Outdated
Comment on lines 566 to 571
if getattr(res, "exit_code", 0) != 0:
logging.warning(
"[SWEEnv] Workspace setup exit code %s: %s",
res.exit_code,
res.stderr or res.stdout,
getattr(res, "stderr", None) or getattr(res, "stdout", 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.

medium

Using speculative getattr with defaults on res (which is a CommandResult object returned by workspace.execute_command) violates the 'One well-lit path' philosophy in the style guide. Since res has a stable, strictly typed contract with exit_code, stderr, and stdout attributes, please access them directly.

Suggested change
if getattr(res, "exit_code", 0) != 0:
logging.warning(
"[SWEEnv] Workspace setup exit code %s: %s",
res.exit_code,
res.stderr or res.stdout,
getattr(res, "stderr", None) or getattr(res, "stdout", None),
)
if res.exit_code != 0:
logging.warning(
"[SWEEnv] Workspace setup exit code %s: %s",
res.exit_code,
res.stderr or res.stdout,
)
References
  1. One well-lit path (No defensive fallbacks): Never use speculative getattr(obj, 'field', default), dict.get('key', default) on structured schemas... Access attributes and keys directly (obj.field) on a single, strictly typed contract. (link)

Comment thread examples/deepswe/openhands_utils.py Outdated
else:
logging.info("[SWEEnv] Successfully configured OpenHands workspace")
if isinstance(entry, dict) and not entry.get("base_commit"):
stdout = getattr(res, "stdout", 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.

medium

Using speculative getattr with defaults on res violates the 'One well-lit path' philosophy. Please access res.stdout directly.

Suggested change
stdout = getattr(res, "stdout", None)
stdout = res.stdout
References
  1. One well-lit path (No defensive fallbacks): Never use speculative getattr(obj, 'field', default), dict.get('key', default) on structured schemas... Access attributes and keys directly (obj.field) on a single, strictly typed contract. (link)

Comment thread examples/deepswe/openhands_utils.py Outdated
Comment on lines +594 to +597
if getattr(result, "timeout_occurred", False) is True:
return True
exit_code = getattr(result, "exit_code", None)
return exit_code == -1 and elapsed >= 0.95 * float(timeout)

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.

medium

Using speculative getattr with defaults on result violates the 'One well-lit path' philosophy. Please access result.timeout_occurred and result.exit_code directly.

Suggested change
if getattr(result, "timeout_occurred", False) is True:
return True
exit_code = getattr(result, "exit_code", None)
return exit_code == -1 and elapsed >= 0.95 * float(timeout)
if result.timeout_occurred is True:
return True
exit_code = result.exit_code
return exit_code == -1 and elapsed >= 0.95 * float(timeout)
References
  1. One well-lit path (No defensive fallbacks): Never use speculative getattr(obj, 'field', default), dict.get('key', default) on structured schemas... Access attributes and keys directly (obj.field) on a single, strictly typed contract. (link)

@niting
niting force-pushed the niting/openhands-mlperf-v61 branch from b9e3702 to d9ec86c Compare October 3, 2026 08:20
@niting niting closed this Oct 3, 2026
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