feat: stage per-trial browser challenge workspaces
This commit is contained in:
@@ -12,15 +12,17 @@ examples/browser_click_workflow/
|
|||||||
## Default Behavior
|
## Default Behavior
|
||||||
|
|
||||||
By default the harness does not start a `wf-rpc-server`. It prompts agents to
|
By default the harness does not start a `wf-rpc-server`. It prompts agents to
|
||||||
use the configured local CLI path:
|
use a per-trial configured local CLI path:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
uv run wf --config examples/browser_click_workflow/wf.config.json --local
|
uv run wf --config examples/agent_challenges/browser_click_challenge/workspaces/<trial>/wf.config.json --local
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For each local-mode trial, the harness copies `workspace_template/` into
|
||||||
|
`workspaces/<model>-trial-<n>/` and injects that config path into the prompt.
|
||||||
This builds the configured workflow server in the CLI process for each command
|
This builds the configured workflow server in the CLI process for each command
|
||||||
and reuses the configured durable store. It does not reuse in-memory source
|
and uses the copied workspace's durable store. It does not reuse in-memory
|
||||||
sessions across CLI invocations.
|
source sessions across CLI invocations.
|
||||||
|
|
||||||
Use `--start-server` when the trial should exercise the JSON-RPC server path.
|
Use `--start-server` when the trial should exercise the JSON-RPC server path.
|
||||||
With `--start-server`, the harness starts:
|
With `--start-server`, the harness starts:
|
||||||
@@ -60,7 +62,8 @@ examples/agent_challenges/browser_click_challenge/workspace_template/
|
|||||||
|
|
||||||
It contains a local workflow config and prompt template that point at the
|
It contains a local workflow config and prompt template that point at the
|
||||||
browser-click Python source without exposing a generated draft patch answer
|
browser-click Python source without exposing a generated draft patch answer
|
||||||
file. Use it as the starting context for an agent trial, or copy it under:
|
file. The harness copies it automatically for normal local-mode trials. For
|
||||||
|
manual experiments, copy it under:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
examples/agent_challenges/browser_click_challenge/workspaces/
|
examples/agent_challenges/browser_click_challenge/workspaces/
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
@@ -25,6 +26,8 @@ ROOT = Path(__file__).resolve().parents[3]
|
|||||||
CHALLENGE_DIR = Path(__file__).resolve().parent
|
CHALLENGE_DIR = Path(__file__).resolve().parent
|
||||||
DEFAULT_PROMPT = CHALLENGE_DIR / "prompt.md"
|
DEFAULT_PROMPT = CHALLENGE_DIR / "prompt.md"
|
||||||
DEFAULT_RESULTS_DIR = CHALLENGE_DIR / "results"
|
DEFAULT_RESULTS_DIR = CHALLENGE_DIR / "results"
|
||||||
|
DEFAULT_WORKSPACES_DIR = CHALLENGE_DIR / "workspaces"
|
||||||
|
DEFAULT_WORKSPACE_TEMPLATE = CHALLENGE_DIR / "workspace_template"
|
||||||
DEFAULT_SERVER_PORT = 8772
|
DEFAULT_SERVER_PORT = 8772
|
||||||
EXAMPLE_CONFIG = ROOT / "examples" / "browser_click_workflow" / "wf.config.json"
|
EXAMPLE_CONFIG = ROOT / "examples" / "browser_click_workflow" / "wf.config.json"
|
||||||
EXAMPLE_CONFIG_ARG = "examples/browser_click_workflow/wf.config.json"
|
EXAMPLE_CONFIG_ARG = "examples/browser_click_workflow/wf.config.json"
|
||||||
@@ -68,6 +71,15 @@ class ManagedServer:
|
|||||||
rpc_url: str
|
rpc_url: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class TrialWorkspace:
|
||||||
|
"""Per-trial scratch area copied from the challenge workspace template."""
|
||||||
|
|
||||||
|
root: Path
|
||||||
|
config_path: Path
|
||||||
|
prompt_path: Path
|
||||||
|
|
||||||
|
|
||||||
def start_server(*, port: int, timeout_seconds: int = 30) -> ManagedServer:
|
def start_server(*, port: int, timeout_seconds: int = 30) -> ManagedServer:
|
||||||
command = server_command(port=port)
|
command = server_command(port=port)
|
||||||
process = subprocess.Popen(
|
process = subprocess.Popen(
|
||||||
@@ -105,6 +117,42 @@ def wait_for_status(*, rpc_url: str, timeout_seconds: int) -> None:
|
|||||||
raise RuntimeError(f"wf status did not become ready: {last_stderr}")
|
raise RuntimeError(f"wf status did not become ready: {last_stderr}")
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_model_name(model: str) -> str:
|
||||||
|
return model.replace("/", "_").replace(":", "_")
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_trial_workspace(
|
||||||
|
*,
|
||||||
|
model: str,
|
||||||
|
index: int,
|
||||||
|
workspaces_dir: Path = DEFAULT_WORKSPACES_DIR,
|
||||||
|
template_dir: Path = DEFAULT_WORKSPACE_TEMPLATE,
|
||||||
|
) -> TrialWorkspace:
|
||||||
|
"""Copy the authoring template into a clean ignored per-trial directory."""
|
||||||
|
root = workspaces_dir / f"{_safe_model_name(model)}-trial-{index:03d}"
|
||||||
|
if root.exists():
|
||||||
|
# Stale scratch files can leak answers between trials, so reset only
|
||||||
|
# the ignored per-trial directory before copying the template.
|
||||||
|
shutil.rmtree(root)
|
||||||
|
shutil.copytree(template_dir, root)
|
||||||
|
return TrialWorkspace(
|
||||||
|
root=root,
|
||||||
|
config_path=root / "wf.config.json",
|
||||||
|
prompt_path=root / "prompt.md",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def wf_command_prefix_for_config(config_path: Path) -> str:
|
||||||
|
path = config_path
|
||||||
|
if not path.is_absolute():
|
||||||
|
path = ROOT / path
|
||||||
|
try:
|
||||||
|
path_arg = path.resolve().relative_to(ROOT.resolve()).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
path_arg = str(path.resolve())
|
||||||
|
return f"uv run wf --config {path_arg} --local"
|
||||||
|
|
||||||
|
|
||||||
def stop_server(process: subprocess.Popen[str]) -> None:
|
def stop_server(process: subprocess.Popen[str]) -> None:
|
||||||
if process.poll() is not None:
|
if process.poll() is not None:
|
||||||
return
|
return
|
||||||
@@ -334,8 +382,7 @@ def _contains_bool_marker(text: str, marker: str, value: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def trial_output_path(results_dir: Path, *, model: str, index: int) -> Path:
|
def trial_output_path(results_dir: Path, *, model: str, index: int) -> Path:
|
||||||
safe_model = model.replace("/", "_").replace(":", "_")
|
return results_dir / f"{_safe_model_name(model)}-trial-{index:03d}.json"
|
||||||
return results_dir / f"{safe_model}-trial-{index:03d}.json"
|
|
||||||
|
|
||||||
|
|
||||||
def run_trial(config: TrialConfig, *, index: int, results_dir: Path) -> dict[str, Any]:
|
def run_trial(config: TrialConfig, *, index: int, results_dir: Path) -> dict[str, Any]:
|
||||||
@@ -433,6 +480,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
)
|
)
|
||||||
parser.add_argument("--prompt", type=Path, default=DEFAULT_PROMPT)
|
parser.add_argument("--prompt", type=Path, default=DEFAULT_PROMPT)
|
||||||
parser.add_argument("--results-dir", type=Path, default=DEFAULT_RESULTS_DIR)
|
parser.add_argument("--results-dir", type=Path, default=DEFAULT_RESULTS_DIR)
|
||||||
|
parser.add_argument("--workspaces-dir", type=Path, default=DEFAULT_WORKSPACES_DIR)
|
||||||
parser.add_argument("--server-url", default=None)
|
parser.add_argument("--server-url", default=None)
|
||||||
parser.add_argument("--start-server", action="store_true", default=False)
|
parser.add_argument("--start-server", action="store_true", default=False)
|
||||||
parser.add_argument("--no-start-server", action="store_false", dest="start_server")
|
parser.add_argument("--no-start-server", action="store_false", dest="start_server")
|
||||||
@@ -464,18 +512,37 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
use_trial_workspace = args.server_url is None and not args.start_server
|
||||||
|
summaries: list[dict[str, Any]] = []
|
||||||
|
for index in range(1, args.trials + 1):
|
||||||
|
prompt_path = args.prompt
|
||||||
|
trial_wf_command_prefix = wf_command_prefix
|
||||||
|
trial_server_context = server_context
|
||||||
|
if use_trial_workspace:
|
||||||
|
workspace = prepare_trial_workspace(
|
||||||
|
model=args.model,
|
||||||
|
index=index,
|
||||||
|
workspaces_dir=args.workspaces_dir,
|
||||||
|
)
|
||||||
|
if args.prompt == DEFAULT_PROMPT:
|
||||||
|
prompt_path = workspace.prompt_path
|
||||||
|
trial_wf_command_prefix = wf_command_prefix_for_config(
|
||||||
|
workspace.config_path
|
||||||
|
)
|
||||||
|
trial_server_context = (
|
||||||
|
"No external workflow RPC server is staged. Use the "
|
||||||
|
"per-trial workspace config copied to "
|
||||||
|
f"`{workspace.config_path.relative_to(ROOT).as_posix()}`."
|
||||||
|
)
|
||||||
config = TrialConfig(
|
config = TrialConfig(
|
||||||
model=args.model,
|
model=args.model,
|
||||||
variant=args.variant,
|
variant=args.variant,
|
||||||
prompt_path=args.prompt,
|
prompt_path=prompt_path,
|
||||||
attach_url=args.attach_url,
|
attach_url=args.attach_url,
|
||||||
timeout_seconds=args.timeout_seconds,
|
timeout_seconds=args.timeout_seconds,
|
||||||
wf_command_prefix=wf_command_prefix,
|
wf_command_prefix=trial_wf_command_prefix,
|
||||||
server_context=server_context,
|
server_context=trial_server_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
summaries: list[dict[str, Any]] = []
|
|
||||||
for index in range(1, args.trials + 1):
|
|
||||||
result = run_trial(config, index=index, results_dir=args.results_dir)
|
result = run_trial(config, index=index, results_dir=args.results_dir)
|
||||||
summaries.append(
|
summaries.append(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ Build and successfully run a workflow that:
|
|||||||
Use this command prefix for product-facing operations:
|
Use this command prefix for product-facing operations:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
uv run wf --config examples/agent_challenges/browser_click_challenge/workspace_template/wf.config.json --local
|
{{wf_command_prefix}}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
{{server_context}}
|
||||||
|
|
||||||
Start from `skills/`, especially the workflow/CLI skill references, before doing
|
Start from `skills/`, especially the workflow/CLI skill references, before doing
|
||||||
broad docs or code search. Do not read outside this repository. Do not solve the
|
broad docs or code search. Do not read outside this repository. Do not solve the
|
||||||
challenge with only a standalone Playwright/Python script.
|
challenge with only a standalone Playwright/Python script.
|
||||||
@@ -70,4 +72,3 @@ the `wf` CLI, either in local same-process mode or through `wf-rpc-server`. Set
|
|||||||
`WorkflowApi` directly.
|
`WorkflowApi` directly.
|
||||||
|
|
||||||
If something fails, report the exact command and error instead of hiding it.
|
If something fails, report the exact command and error instead of hiding it.
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ from examples.agent_challenges.browser_click_challenge.run_opencode_trials impor
|
|||||||
classify_output,
|
classify_output,
|
||||||
extract_challenge_report,
|
extract_challenge_report,
|
||||||
parse_opencode_output,
|
parse_opencode_output,
|
||||||
|
prepare_trial_workspace,
|
||||||
render_prompt,
|
render_prompt,
|
||||||
server_command,
|
server_command,
|
||||||
trial_output_path,
|
trial_output_path,
|
||||||
|
wf_command_prefix_for_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -247,6 +249,71 @@ def test_trial_output_path_is_zero_padded(tmp_path: Path) -> None:
|
|||||||
assert path.name == "opencode_mimo-v2.5-free-trial-003.json"
|
assert path.name == "opencode_mimo-v2.5-free-trial-003.json"
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_trial_workspace_copies_template_to_model_trial_dir(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
template = tmp_path / "template"
|
||||||
|
template.mkdir()
|
||||||
|
(template / "wf.config.json").write_text('{"version": 1}', encoding="utf-8")
|
||||||
|
(template / "prompt.md").write_text("prompt", encoding="utf-8")
|
||||||
|
(template / ".gitignore").write_text(".wf_store/\n", encoding="utf-8")
|
||||||
|
workspaces = tmp_path / "workspaces"
|
||||||
|
|
||||||
|
prepared = prepare_trial_workspace(
|
||||||
|
model="opencode/mimo-v2.5-free",
|
||||||
|
index=7,
|
||||||
|
workspaces_dir=workspaces,
|
||||||
|
template_dir=template,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert prepared.root == workspaces / "opencode_mimo-v2.5-free-trial-007"
|
||||||
|
assert prepared.config_path.read_text(encoding="utf-8") == '{"version": 1}'
|
||||||
|
assert prepared.prompt_path.read_text(encoding="utf-8") == "prompt"
|
||||||
|
assert (prepared.root / ".gitignore").read_text(encoding="utf-8") == ".wf_store/\n"
|
||||||
|
|
||||||
|
|
||||||
|
def test_prepare_trial_workspace_removes_stale_previous_attempt(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
template = tmp_path / "template"
|
||||||
|
template.mkdir()
|
||||||
|
(template / "wf.config.json").write_text('{"version": 1}', encoding="utf-8")
|
||||||
|
(template / "prompt.md").write_text("prompt", encoding="utf-8")
|
||||||
|
workspaces = tmp_path / "workspaces"
|
||||||
|
stale = workspaces / "opencode_mimo-v2.5-free-trial-001" / "old-answer.json"
|
||||||
|
stale.parent.mkdir(parents=True)
|
||||||
|
stale.write_text("stale", encoding="utf-8")
|
||||||
|
|
||||||
|
prepared = prepare_trial_workspace(
|
||||||
|
model="opencode/mimo-v2.5-free",
|
||||||
|
index=1,
|
||||||
|
workspaces_dir=workspaces,
|
||||||
|
template_dir=template,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert prepared.root.exists()
|
||||||
|
assert not stale.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_wf_command_prefix_for_config_uses_repo_relative_path() -> None:
|
||||||
|
config_path = (
|
||||||
|
Path("examples")
|
||||||
|
/ "agent_challenges"
|
||||||
|
/ "browser_click_challenge"
|
||||||
|
/ "workspaces"
|
||||||
|
/ "opencode_mimo-v2.5-free-trial-001"
|
||||||
|
/ "wf.config.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
prefix = wf_command_prefix_for_config(config_path)
|
||||||
|
|
||||||
|
assert prefix == (
|
||||||
|
"uv run wf --config "
|
||||||
|
"examples/agent_challenges/browser_click_challenge/workspaces/"
|
||||||
|
"opencode_mimo-v2.5-free-trial-001/wf.config.json --local"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_render_prompt_injects_command_prefix_and_server_context(
|
def test_render_prompt_injects_command_prefix_and_server_context(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user