feat(tester): initialize agent target from Orca selectors

This commit is contained in:
2026-10-12 01:14:52 +08:00
parent 5ba1d8e3d6
commit a35cc916cb
9 changed files with 688 additions and 10 deletions
+88 -1
View File
@@ -37,6 +37,18 @@ EXPECTED_KINDS = (
"url",
"interaction",
)
AGENT_DEFAULTS = {
"environment": "",
"host": "local",
"project": "",
"path": "",
"role": "test",
"cli": "codex",
"tier": "standard",
"model": None,
"reasoningEffort": "medium",
"permissionMode": "workspace-write",
}
def catalog_dir(root: Path) -> Path:
@@ -84,6 +96,41 @@ def load_local(root: Path) -> dict[str, Any]:
return data
def resolve_agent(root: Path, meta: dict[str, Any]) -> dict[str, Any]:
"""Resolve the structured agent target used by one tester run.
Stable defaults may live in catalog.yaml. Machine-specific values belong in
local.yaml and override the mapping shallowly. The old execution mapping is
accepted as a compatibility input and is projected onto the agent target.
"""
agent: dict[str, Any] = dict(AGENT_DEFAULTS)
local = load_local(root)
found_agent = False
for source_name, source in (
("catalog.agent", meta.get("agent")),
("local.agent", local.get("agent")),
):
if source is None:
continue
found_agent = True
if not isinstance(source, dict):
raise SystemExit(f"{source_name} 必须是 mapping")
agent.update(source)
if not found_agent:
for source_name, source in (
("catalog.execution", meta.get("execution")),
("local.execution", local.get("execution")),
):
if source is None:
continue
if not isinstance(source, dict):
raise SystemExit(f"{source_name} 必须是 mapping")
for key in ("environment", "host", "path"):
if key in source:
agent[key] = source[key]
return agent
def case_files(root: Path) -> list[Path]:
d = catalog_dir(root) / "cases"
if not d.is_dir():
@@ -143,7 +190,7 @@ def resolve_vars(root: Path, meta: dict[str, Any]) -> dict[str, str]:
merged[str(key)] = "" if value is None else str(value)
local = load_local(root)
for key, value in local.items():
if value is None:
if value is None or isinstance(value, (dict, list)):
continue
merged[str(key)] = str(value).lower() if isinstance(value, bool) else str(value)
auth_keys = set()
@@ -338,6 +385,44 @@ def probe_errors(where: str, probe: Any) -> list[str]:
return errors
def agent_errors(root: Path, meta: dict[str, Any]) -> list[str]:
errors: list[str] = []
try:
agent = resolve_agent(root, meta)
except SystemExit as exc:
return [str(exc)]
for key in (
"environment",
"host",
"project",
"path",
"role",
"cli",
"tier",
"model",
"reasoningEffort",
"permissionMode",
):
if key not in agent:
errors.append(f"agent.{key} 缺失")
for key in ("environment", "host", "project", "path", "role", "cli", "tier", "permissionMode"):
if not isinstance(agent.get(key), str):
errors.append(f"agent.{key} 必须是字符串")
if agent.get("model") is not None and not isinstance(agent.get("model"), str):
errors.append("agent.model 必须是字符串或 null")
if agent.get("role") != "test":
errors.append("agent.role 必须是 test")
if agent.get("cli") not in ("codex", "cursor-agent", "grok", "omp", "pi"):
errors.append("agent.cli 只能是 codex/cursor-agent/grok/omp/pi")
if agent.get("tier") not in ("standard", "strong"):
errors.append("agent.tier 只能是 standard 或 strong")
if agent.get("reasoningEffort") not in (None, "low", "medium", "high", "xhigh"):
errors.append("agent.reasoningEffort 只能是 low/medium/high/xhigh 或 null")
if agent.get("permissionMode") not in ("read-only", "workspace-write"):
errors.append("agent.permissionMode 只能是 read-only 或 workspace-write")
return errors
def run_together_groups(meta: dict[str, Any]) -> list[dict[str, Any]]:
raw = meta.get("run_together") or []
if not isinstance(raw, list):
@@ -505,6 +590,7 @@ def cmd_select(args: argparse.Namespace, root: Path) -> int:
"skipped": skipped,
"run_together": together,
"status_probe": status_probes,
"agent": resolve_agent(root, meta),
"cases": selected,
},
ensure_ascii=False,
@@ -533,6 +619,7 @@ def validate(root: Path) -> list[str]:
meta = load_catalog_meta(root)
except SystemExit as exc:
return [str(exc)]
errors.extend(agent_errors(root, meta))
cats = meta.get("categories")
if not isinstance(cats, list):
+258
View File
@@ -0,0 +1,258 @@
#!/usr/bin/env python3
"""Resolve an Orca project or terminal into tester's structured agent config."""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from pathlib import Path
from typing import Any
try:
import yaml
except ImportError: # pragma: no cover
print("需要 PyYAML: python3 -m pip install pyyaml", file=sys.stderr)
raise SystemExit(2)
SUPPORTED_CLI = {"codex", "cursor-agent", "grok", "omp", "pi"}
def run_orca(args: list[str], environment: str | None = None) -> dict[str, Any]:
command = ["orca", *args]
if environment and environment != "local":
command.extend(["--environment", environment])
command.append("--json")
proc = subprocess.run(command, capture_output=True, text=True, check=False)
raw = (proc.stdout or "").strip()
try:
data = json.loads(raw)
except json.JSONDecodeError as exc:
detail = (proc.stderr or raw or str(exc)).strip()
raise RuntimeError(f"orca 命令没有返回 JSON: {' '.join(command)}\n{detail}") from exc
if not data.get("ok"):
error = data.get("error") or {}
message = error.get("message") or error.get("code") or "unknown error"
raise RuntimeError(f"{' '.join(command)}: {message}")
result = data.get("result")
return result if isinstance(result, dict) else {}
def environments() -> list[str]:
result = run_orca(["environment", "list"])
names = ["local"]
for item in result.get("environments") or []:
if isinstance(item, dict) and item.get("name"):
names.append(str(item["name"]))
return names
def worktree_from_terminal(terminal: dict[str, Any], environment: str) -> dict[str, Any]:
worktree_id = str(terminal.get("worktreeId") or "")
if not worktree_id:
raise RuntimeError("terminal 没有 worktreeId")
result = run_orca(["worktree", "show", "--worktree", worktree_id], environment)
worktree = result.get("worktree")
if not isinstance(worktree, dict):
raise RuntimeError("找不到 terminal 对应的 worktree")
return worktree
def config_from_terminal(handle: str, requested_environment: str | None) -> dict[str, Any]:
candidates = [requested_environment] if requested_environment else environments()
errors: list[str] = []
for environment in candidates:
if not environment:
continue
try:
result = run_orca(["terminal", "show", "--terminal", handle], environment)
terminal = result.get("terminal")
if not isinstance(terminal, dict):
raise RuntimeError("返回缺少 terminal")
worktree = worktree_from_terminal(terminal, environment)
identity = str(terminal.get("agentIdentity") or "")
cli = identity if identity in SUPPORTED_CLI else "codex"
return {
"environment": "" if environment == "local" else environment,
"host": str(terminal.get("executionHostId") or worktree.get("hostId") or "local"),
"project": str(worktree.get("projectId") or worktree.get("repoId") or ""),
"path": str(terminal.get("worktreePath") or worktree.get("path") or ""),
"role": "test",
"cli": cli,
"tier": "standard",
"model": None,
"reasoningEffort": "medium",
"permissionMode": "workspace-write",
"_source": f"terminal:{handle}",
"_project_name": str(worktree.get("displayName") or ""),
"_terminal_agent_identity": identity,
}
except RuntimeError as exc:
errors.append(f"{environment}: {exc}")
raise RuntimeError("找不到可用 terminal: " + ";".join(errors))
def worktrees(environment: str) -> list[dict[str, Any]]:
result = run_orca(["worktree", "list", "--limit", "200"], environment)
return [item for item in result.get("worktrees") or [] if isinstance(item, dict)]
def projects(environment: str) -> list[dict[str, Any]]:
result = run_orca(["project", "list"], environment)
return [item for item in result.get("projects") or [] if isinstance(item, dict)]
def config_from_project(
selector: str,
requested_environment: str | None,
requested_path: str | None = None,
) -> dict[str, Any]:
environments_to_check = [requested_environment] if requested_environment else environments()
matches: list[tuple[str, dict[str, Any]]] = []
for environment in environments_to_check:
if not environment:
continue
project_ids: set[str] = set()
for project in projects(environment):
project_values = {
str(project.get("id") or ""),
str(project.get("displayName") or ""),
}
if selector in project_values:
project_ids.add(str(project.get("id") or ""))
project_ids.update(str(repo_id) for repo_id in project.get("sourceRepoIds") or [])
for item in worktrees(environment):
values = {
str(item.get("id") or ""),
str(item.get("projectId") or ""),
str(item.get("repoId") or ""),
str(item.get("displayName") or ""),
str(item.get("path") or ""),
Path(str(item.get("path") or "")).name,
}
direct_match = selector in values
project_match = bool(
project_ids
and project_ids.intersection(
{
str(item.get("projectId") or ""),
str(item.get("repoId") or ""),
}
)
)
path_match = not requested_path or str(item.get("path") or "") == requested_path
if path_match and (direct_match or project_match):
matches.append((environment, item))
if not matches:
raise RuntimeError(f"找不到 project id/名称: {selector}")
if len(matches) > 1:
lines = [
f"{env}: {item.get('projectId') or item.get('repoId')} "
f"{item.get('displayName') or ''} {item.get('path') or ''}"
for env, item in matches
]
raise RuntimeError("project 匹配多个 worktree,请补充 environment 或 path:\n" + "\n".join(lines))
environment, item = matches[0]
return {
"environment": "" if environment == "local" else environment,
"host": str(item.get("hostId") or "local"),
"project": str(item.get("projectId") or item.get("repoId") or item.get("id") or ""),
"path": str(item.get("path") or ""),
"role": "test",
"cli": "codex",
"tier": "standard",
"model": None,
"reasoningEffort": "medium",
"permissionMode": "workspace-write",
"_source": f"project:{selector}",
"_project_name": str(item.get("displayName") or ""),
}
def clean_config(config: dict[str, Any]) -> dict[str, Any]:
return {key: value for key, value in config.items() if not key.startswith("_")}
def write_local(root: Path, config: dict[str, Any]) -> tuple[Path, dict[str, Any]]:
path = root / ".pouch" / "tester" / "local.yaml"
path.parent.mkdir(parents=True, exist_ok=True)
data: dict[str, Any] = {}
if path.is_file():
loaded = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
if not isinstance(loaded, dict):
raise RuntimeError(f"{path} 必须是 mapping")
data = loaded
current_agent = data.get("agent")
if current_agent is None:
current_agent = {}
if not isinstance(current_agent, dict):
raise RuntimeError(f"{path} 的 agent 必须是 mapping")
resolved = clean_config(config)
# Target location always comes from the user's selector. Keep explicit local
# choices for model/policy fields, so resolving a terminal cannot erase them.
location_fields = {"environment", "host", "project", "path"}
terminal_cli = bool(config.get("_terminal_agent_identity"))
merged = dict(current_agent)
for key, value in resolved.items():
if key in location_fields or key not in merged or (key == "cli" and terminal_cli):
merged[key] = value
data["agent"] = merged
path.write_text(yaml.safe_dump(data, allow_unicode=True, sort_keys=False), encoding="utf-8")
return path, merged
def print_confirmation(config: dict[str, Any], written: bool) -> None:
model = config.get("model") or "CLI 默认模型"
print("## tester Agent 配置更新结果" if written else "## tester Agent 配置解析预览")
print()
print(f"来源: {config.get('_source', '配置文件')}")
print(f"Agent: {config.get('cli')}")
print(f"角色 / 档位: {config.get('role')} / {config.get('tier')}")
print(f"模型: {model}")
print(f"推理强度: {config.get('reasoningEffort') or 'n/a'}")
print(f"权限: {config.get('permissionMode') or 'n/a'}")
print(f"Orca 环境: {config.get('environment') or 'local'}")
print(f"执行节点: {config.get('host') or 'local'}")
print(f"项目: {config.get('project') or 'n/a'}({config.get('_project_name') or 'n/a'})")
print(f"目录: {config.get('path') or 'n/a'}")
print("Terminal: 每轮新建,结束后关闭,不复用输入的 terminal")
print()
if written:
print("以上是已写入 local.yaml.agent 的生效配置;如需调整,直接修改配置后重新运行 tester。")
else:
print("以上是解析结果预览;加上 --write 后会写入 local.yaml.agent。")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="解析 Orca project/terminal 为 tester Agent 配置")
parser.add_argument("--root", default=".", help="项目根")
source = parser.add_mutually_exclusive_group(required=True)
source.add_argument("--terminal", help="Orca terminal handle")
source.add_argument("--project", help="Orca project id、名称或路径")
parser.add_argument("--path", help="项目有多个 worktree 时按目标机器上的绝对路径消歧")
parser.add_argument("--environment", help="限定 Orca environment")
parser.add_argument("--write", action="store_true", help="写入 .pouch/tester/local.yaml")
args = parser.parse_args(argv)
try:
config = (
config_from_terminal(args.terminal, args.environment)
if args.terminal
else config_from_project(args.project, args.environment, args.path)
)
if args.write:
path, saved_agent = write_local(Path(args.root).resolve(), config)
config = {**config, **saved_agent}
print(f"已写入: {path}")
print_confirmation(config, written=args.write)
return 0
except RuntimeError as exc:
print(f"environment-fail: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())