Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/benchflow/agents/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,28 @@ async def install_agent(
diagnostics=diag.stdout or "",
log_path=str(install_log),
)
if agent_cfg and agent_cfg.install_setup_cmd:
setup_cmd = agent_cfg.install_setup_cmd
setup_result = await env.exec(setup_cmd, timeout_sec=install_timeout)
setup_parts = [f"$ {setup_cmd}\n"]
if setup_result.stdout:
setup_parts.extend(["=== stdout ===\n", setup_result.stdout])
if not setup_result.stdout.endswith("\n"):
setup_parts.append("\n")
if setup_result.stderr:
setup_parts.extend(["=== stderr ===\n", setup_result.stderr])
if not setup_result.stderr.endswith("\n"):
setup_parts.append("\n")
with install_log.open("a") as handle:
handle.write("".join(setup_parts))
if setup_result.return_code != 0:
raise AgentInstallError(
agent=agent_base,
return_code=setup_result.return_code,
stdout="".join(setup_parts),
diagnostics="post-install setup failed",
log_path=str(install_log),
)
return agent_cfg


Expand Down
2 changes: 2 additions & 0 deletions src/benchflow/agents/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
"disallow_web_tools_launch_suffix",
"task_mcp_transport",
"task_mcp_config_path",
"install_setup_cmd",
"launch_override_cmd",
}
)

Expand Down
133 changes: 133 additions & 0 deletions src/benchflow/agents/openhands_settings_writer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""Standalone OpenHands settings-writer source embedded into sandboxes."""

OPENHANDS_SETTINGS_WRITER = r"""import json
import os
import shutil
import subprocess
import sys
from pathlib import Path


def optional_int(name):
value = os.environ.get(name, "").strip()
if not value:
return None
parsed = int(value)
if parsed <= 0:
raise ValueError(f"{name} must be positive")
return parsed


def optional_non_negative_int(name):
value = os.environ.get(name, "").strip()
if not value:
return None
if not value.isdigit():
raise ValueError(f"{name} must be a non-negative integer")
return int(value)


def optional_bool(name):
value = os.environ.get(name, "").strip().lower()
if not value:
return None
if value in {"1", "true", "yes"}:
return True
if value in {"0", "false", "no"}:
return False
raise ValueError(f"{name} must be a boolean")


def disable_subagents_if_requested():
if os.environ.get("BENCHFLOW_OPENHANDS_DISABLE_SUBAGENTS", "0") != "1":
return

openhands_bin = shutil.which("openhands")
if not openhands_bin:
raise RuntimeError("Cannot locate OpenHands executable")
openhands_python = Path(os.path.realpath(openhands_bin)).parent / "python"
if not openhands_python.is_file() or not os.access(openhands_python, os.X_OK):
raise RuntimeError("Cannot locate OpenHands tool interpreter")

subprocess.run(
[
str(openhands_python),
"-c",
"from pathlib import Path\n"
"import openhands_cli.utils as u\n"
"p = Path(u.__file__)\n"
"s = p.read_text()\n"
"old = ' Tool(name=task_tool_name),\\n'\n"
"new = ' # BenchFlow: delegation disabled for this run.\\n'\n"
"assert old in s or new in s\n"
"p.write_text(s.replace(old, new, 1))\n",
],
check=True,
)


llm = {
"model": os.environ["LLM_MODEL"],
"api_key": os.environ["LLM_API_KEY"],
"usage_id": "agent",
}
for env_name, field_name in (
("LLM_BASE_URL", "base_url"),
("LLM_API_VERSION", "api_version"),
):
value = os.environ.get(env_name, "").strip()
if value:
llm[field_name] = value

for env_name, field_name in (
("LLM_NATIVE_TOOL_CALLING", "native_tool_calling"),
("LLM_CACHING_PROMPT", "caching_prompt"),
("LLM_DROP_PARAMS", "drop_params"),
("LLM_MODIFY_PARAMS", "modify_params"),
):
value = optional_bool(env_name)
if value is not None:
llm[field_name] = value

timeout = optional_non_negative_int("LLM_TIMEOUT")
if timeout is not None:
llm["timeout"] = timeout

reasoning_effort = os.environ.get("LLM_REASONING_EFFORT", "").strip()
if reasoning_effort == "max":
llm["litellm_extra_body"] = {"reasoning": {"effort": "max"}}
elif reasoning_effort in {"none", "low", "medium", "high", "xhigh"}:
llm["reasoning_effort"] = reasoning_effort
llm["litellm_extra_body"] = {"reasoning_effort": reasoning_effort}
elif reasoning_effort:
llm["litellm_extra_body"] = {"reasoning_effort": reasoning_effort}

context_limit = optional_int("BENCHFLOW_OPENHANDS_CONTEXT_LIMIT")
output_limit = optional_int("BENCHFLOW_OPENHANDS_OUTPUT_LIMIT")
if context_limit is not None:
llm["max_input_tokens"] = context_limit
if output_limit is not None:
llm["max_output_tokens"] = output_limit

condenser = {
"llm": {**llm, "usage_id": "condenser"},
"max_size": 80,
"keep_first": 4,
"kind": "LLMSummarizingCondenser",
}
if context_limit is not None and output_limit is not None:
reserve = optional_int("BENCHFLOW_OPENHANDS_CONTEXT_RESERVE") or 4096
condenser_limit = context_limit - output_limit - reserve
if condenser_limit <= 0:
raise ValueError("OpenHands context budget leaves no room for input")
condenser["max_tokens"] = condenser_limit

settings = {
"llm": llm,
"tools": [],
"condenser": condenser,
"kind": "Agent",
}
Path(sys.argv[1]).write_text(json.dumps(settings, separators=(",", ":")))
disable_subagents_if_requested()
"""
20 changes: 20 additions & 0 deletions src/benchflow/agents/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
from dataclasses import dataclass, field
from pathlib import Path

from benchflow.agents.openhands_settings_writer import (
OPENHANDS_SETTINGS_WRITER as _OPENHANDS_SETTINGS_WRITER,
)


def _install_python_script(container_path: str, source: str) -> str:
"""Shell snippet that ensures python3 and writes `source` to container_path.
Expand Down Expand Up @@ -120,6 +124,7 @@ def _apt_install(*packages: str) -> str:
_OPENHANDS_CLI_GIT_REV = "2df8a2835d3f1bd2f2eadf5a7a2e1ad0dfb0d271"
_OPENHANDS_SDK_VERSION = "1.28.1"
_OPENHANDS_TOOLS_VERSION = "1.28.1"
_OPENHANDS_SETTINGS_WRITER_PATH = "/opt/benchflow/bin/openhands-settings-writer"
_JS_AGENT_PATH = (
f"{_BENCHFLOW_BIN_PREFIX}:{_BENCHFLOW_JS_AGENT_PREFIX}/bin:"
f"{_BENCHFLOW_NODE_PREFIX}/bin:$PATH"
Expand Down Expand Up @@ -508,6 +513,11 @@ class AgentConfig:
task_mcp_transport: str = "acp"
# Native-config target path, relative to $HOME unless absolute.
task_mcp_config_path: str = ""
# Host-owned install step for provider/harness compatibility shims that
# cannot be represented in the data-only agent manifest contract.
install_setup_cmd: str = ""
# Host-owned launch override paired with install_setup_cmd.
launch_override_cmd: str = ""


# Agent registry — all supported agents
Expand Down Expand Up @@ -966,6 +976,16 @@ class AgentConfig:
"fi && "
"openhands acp --always-approve --override-with-envs"
),
install_setup_cmd=_install_python_script(
_OPENHANDS_SETTINGS_WRITER_PATH, _OPENHANDS_SETTINGS_WRITER
),
launch_override_cmd=(
'export PATH="$HOME/.local/bin:$PATH" && '
"mkdir -p ~/.openhands && "
f"python3 {_OPENHANDS_SETTINGS_WRITER_PATH} "
"~/.openhands/agent_settings.json && "
"openhands acp --always-approve --override-with-envs"
),
protocol="acp",
requires_env=["LLM_API_KEY"],
api_protocol="",
Expand Down
3 changes: 2 additions & 1 deletion src/benchflow/cli/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ def agent_show(
console.print(f" Aliases: {', '.join(aliases)}")
console.print(f" Description: {cfg.description}")
console.print(f" Protocol: {cfg.protocol}")
console.print(f" Launch: {cfg.launch_cmd}")
launch_cmd = cfg.launch_override_cmd or cfg.launch_cmd
console.print(f" Launch: {launch_cmd}")
console.print(f" Requires: {_format_requires(cfg) or '(none)'}")
console.print(f" Provider auth: {_PROVIDER_AUTH_MESSAGE}")
if cfg.subscription_auth:
Expand Down
8 changes: 6 additions & 2 deletions src/benchflow/rollout_planes.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@ class DefaultRolloutPlanes:
"""Default bindings for the four concrete planes."""

def agent_launch(self, agent: str, *, disallow_web_tools: bool) -> str:
launch = AGENT_LAUNCH.get(agent, agent)
agent_cfg = AGENTS.get(agent)
launch = (
agent_cfg.launch_override_cmd
if agent_cfg and agent_cfg.launch_override_cmd
else AGENT_LAUNCH.get(agent, agent)
)
if not disallow_web_tools:
return launch
agent_cfg = AGENTS.get(agent)
if agent_cfg and agent_cfg.disallow_web_tools_launch_suffix:
return launch + agent_cfg.disallow_web_tools_launch_suffix
return launch
Expand Down
2 changes: 1 addition & 1 deletion src/benchflow/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ def launch_cmd(self) -> str:
config = self.config
if config is None:
return self.name
return config.launch_cmd
return config.launch_override_cmd or config.launch_cmd

def __repr__(self) -> str:
return f"Agent({self.name!r}, model={self.model!r})"
Expand Down
10 changes: 10 additions & 0 deletions tests/test_agent_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,13 @@ def test_agent_show_mentions_provider_specific_azure_auth() -> None:
assert "Provider auth:" in output
assert "AZURE_API_KEY" in output
assert "AZURE_API_ENDPOINT" in output


def test_agent_show_uses_launch_override_when_present() -> None:
"""Guards PR #927: agent diagnostics show the effective runtime launch."""
result = CliRunner().invoke(app, ["agent", "show", "openhands"])

assert result.exit_code == 0
output = click.unstyle(result.output)
assert "Launch:" in output
assert "/opt/benchflow/bin/openhands-settings-writer" in output
Loading
Loading