eval-harness 0.2.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1046 -0
- package/README.zh-CN.md +597 -0
- package/bin/eval-harness.js +65 -0
- package/config/eval.example.json +70 -0
- package/docs/agent-runner-compatibility.md +47 -0
- package/docs/runners.md +82 -0
- package/package.json +43 -0
- package/prompts/judge.md +27 -0
- package/pyproject.toml +48 -0
- package/schemas/batch-comparison.schema.json +64 -0
- package/schemas/batch-summary.schema.json +85 -0
- package/schemas/evaluation.schema.json +81 -0
- package/schemas/experiment.schema.json +63 -0
- package/schemas/history-index.schema.json +37 -0
- package/schemas/history-record.schema.json +56 -0
- package/schemas/model-report.schema.json +38 -0
- package/schemas/next-action.schema.json +62 -0
- package/schemas/skill-improvement-input.schema.json +65 -0
- package/src/eval/__init__.py +3 -0
- package/src/eval/__main__.py +5 -0
- package/src/eval/cli.py +11150 -0
- package/src/eval/runners/__init__.py +0 -0
- package/src/eval/runners/base.py +197 -0
- package/src/eval/runners/claude.py +516 -0
- package/src/eval/runners/codex.py +412 -0
- package/src/eval/runners/mimocode.py +676 -0
- package/src/eval/runners/opencode.py +554 -0
- package/src/eval/schema.py +151 -0
|
File without changes
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
DEFAULT_AGENT_PROMPT = "\n".join(
|
|
9
|
+
[
|
|
10
|
+
"Read and execute work/INSTRUCTION.md. Work autonomously, do not ask for human input, and write all required result artifacts.",
|
|
11
|
+
"",
|
|
12
|
+
"Harness execution contract:",
|
|
13
|
+
"- Treat AGENT_EVAL_TIMEOUT_MINUTES as the hard worker budget.",
|
|
14
|
+
"- Reserve at least AGENT_EVAL_FINALIZE_BEFORE_MINUTES minutes for finalization.",
|
|
15
|
+
"- Always finish by writing work/result/output.md, work/reports/FINAL_RESULT.json, and work/reports/FINAL_RESULT.md, even when blocked or partial.",
|
|
16
|
+
"- Workspace layout: current directory is the evaluation workspace; task code is under project/ and the submitted work package is under work/.",
|
|
17
|
+
"- Runtime artifacts must stay under work/result, work/reports, and work/logs/trace. Do not use root-level result/, reports/, or logs/trace/ paths.",
|
|
18
|
+
"- When delegating to subagents, pass workspace-relative work artifact paths with the work/ prefix.",
|
|
19
|
+
"- Keep large audits chunked: read existing handoff/index artifacts first, process only a small batch of source files or flow families per turn, and write progress artifacts before continuing.",
|
|
20
|
+
"- Do not run helper/finalization scripts through truncating pipes such as Select-Object -First or head. Write full helper output to work/logs/trace first, then preview that saved log separately.",
|
|
21
|
+
"- Before final response, verify the required work/result, work/reports, and work/logs/trace artifacts exist under the work/ prefix.",
|
|
22
|
+
"- Final agent responses should be concise status plus artifact paths; put large reports in work/reports or work/logs/trace.",
|
|
23
|
+
"- If running on Windows, write Java and source files as UTF-8 without BOM.",
|
|
24
|
+
"- Avoid PowerShell Out-File -Encoding utf8 for Java/source files; use utf8NoBOM or an explicit UTF8Encoding(false) writer.",
|
|
25
|
+
]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
DEFAULT_ATTACHED_INSTRUCTION_PROMPT = DEFAULT_AGENT_PROMPT.replace(
|
|
30
|
+
"work/INSTRUCTION.md", "the attached INSTRUCTION.md", 1
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AgentRunner:
|
|
35
|
+
"""Abstract base for agent runner adapters (OpenCode, MiMo Code, etc.)."""
|
|
36
|
+
|
|
37
|
+
# -- identity
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def name(self) -> str:
|
|
41
|
+
"""Machine-readable runner identifier (e.g. 'opencode', 'mimocode')."""
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def binary(self) -> str:
|
|
46
|
+
"""CLI binary name (e.g. 'opencode', 'mimo')."""
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
|
|
49
|
+
# -- config keys used in config JSON
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def config_key(self) -> str:
|
|
53
|
+
"""Key in config for runner-specific settings (e.g. 'opencode', 'mimocode')."""
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
|
|
56
|
+
# -- command building
|
|
57
|
+
|
|
58
|
+
def default_command(self, config: dict[str, Any]) -> list[str]:
|
|
59
|
+
"""Default CLI command template."""
|
|
60
|
+
raise NotImplementedError
|
|
61
|
+
|
|
62
|
+
def command_with_model(self, command: list[str], model: str) -> list[str]:
|
|
63
|
+
"""Inject --model into the command."""
|
|
64
|
+
return _command_set_named_arg(command, "--model", model)
|
|
65
|
+
|
|
66
|
+
def command_with_variant(self, command: list[str], variant: str) -> list[str]:
|
|
67
|
+
"""Inject the variant/agent flag. Override per runner."""
|
|
68
|
+
return command
|
|
69
|
+
|
|
70
|
+
def model_from_command(self, command: list[str]) -> str:
|
|
71
|
+
"""Extract model from command line."""
|
|
72
|
+
return _command_get_named_arg(command, "--model")
|
|
73
|
+
|
|
74
|
+
# -- log directory
|
|
75
|
+
|
|
76
|
+
def log_dir_name(self) -> str:
|
|
77
|
+
"""Subdirectory under run_dir for agent logs/sessions/config."""
|
|
78
|
+
raise NotImplementedError
|
|
79
|
+
|
|
80
|
+
# -- asset installation
|
|
81
|
+
|
|
82
|
+
def default_asset_installs(self) -> list[dict[str, Any]]:
|
|
83
|
+
"""Default asset installation specs (skills, agents, versions)."""
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
86
|
+
# -- runtime profile seeding
|
|
87
|
+
|
|
88
|
+
def runtime_profile_candidates(
|
|
89
|
+
self, base_env: dict[str, str], runtime: dict[str, str]
|
|
90
|
+
) -> list[tuple[Path | None, Path, str]]:
|
|
91
|
+
"""Return (source, dest, label) tuples for seeding runtime profile."""
|
|
92
|
+
raise NotImplementedError
|
|
93
|
+
|
|
94
|
+
# -- provider config
|
|
95
|
+
|
|
96
|
+
def write_agent_config(self, manifest: dict[str, Any]) -> str | None:
|
|
97
|
+
"""Write agent-specific config file. Returns config path or None."""
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
# -- log parsing: sessions
|
|
101
|
+
|
|
102
|
+
def extract_sessions_from_text(
|
|
103
|
+
self, text: str, *, inspector_base_url: str = "http://127.0.0.1:25947"
|
|
104
|
+
) -> dict[str, Any]:
|
|
105
|
+
"""Parse agent log text into session summary."""
|
|
106
|
+
raise NotImplementedError
|
|
107
|
+
|
|
108
|
+
def render_sessions_md(self, summary: dict[str, Any]) -> str:
|
|
109
|
+
"""Render session summary as Markdown."""
|
|
110
|
+
raise NotImplementedError
|
|
111
|
+
|
|
112
|
+
# -- log parsing: model
|
|
113
|
+
|
|
114
|
+
def model_from_log(self, run_dir: Path) -> str:
|
|
115
|
+
"""Extract model name from log file."""
|
|
116
|
+
raise NotImplementedError
|
|
117
|
+
|
|
118
|
+
# -- log parsing: activity
|
|
119
|
+
|
|
120
|
+
def extract_activity_from_text(self, text: str, *, tail_lines: int = 500) -> dict[str, Any]:
|
|
121
|
+
"""Extract recent agent activity from log text."""
|
|
122
|
+
raise NotImplementedError
|
|
123
|
+
|
|
124
|
+
# -- CLI flags
|
|
125
|
+
|
|
126
|
+
def format_flags(self) -> list[str]:
|
|
127
|
+
"""Flags for JSON output format."""
|
|
128
|
+
return ["--format", "json", "--print-logs", "--log-level", "INFO"]
|
|
129
|
+
|
|
130
|
+
def variant_cli_flag(self) -> str:
|
|
131
|
+
"""Name of the CLI flag for variant/agent selection (e.g. '--variant', '--agent')."""
|
|
132
|
+
return ""
|
|
133
|
+
|
|
134
|
+
def variant_from_command(self, command: list[str]) -> str:
|
|
135
|
+
"""Extract variant/agent from command line."""
|
|
136
|
+
flag = self.variant_cli_flag()
|
|
137
|
+
if not flag:
|
|
138
|
+
return ""
|
|
139
|
+
return _command_get_named_arg(command, flag)
|
|
140
|
+
|
|
141
|
+
def default_worker_tools(self, config: dict[str, Any] | None = None) -> list[str]:
|
|
142
|
+
"""Executables Jenkins should check before starting real workers."""
|
|
143
|
+
return [self.binary, "node"]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# -- helpers used by runners
|
|
147
|
+
|
|
148
|
+
def _command_get_named_arg(command: list[str], flag: str) -> str:
|
|
149
|
+
for index, item in enumerate(command):
|
|
150
|
+
text = str(item)
|
|
151
|
+
if text == flag and index + 1 < len(command):
|
|
152
|
+
return str(command[index + 1])
|
|
153
|
+
if text.startswith(f"{flag}="):
|
|
154
|
+
return text.split("=", 1)[1]
|
|
155
|
+
return ""
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _command_set_named_arg(command: list[str], flag: str, value: str) -> list[str]:
|
|
159
|
+
for index, item in enumerate(command):
|
|
160
|
+
text = str(item)
|
|
161
|
+
if text == flag:
|
|
162
|
+
if index + 1 >= len(command):
|
|
163
|
+
raise RuntimeError(f"{flag} is present without a following value")
|
|
164
|
+
result = list(command)
|
|
165
|
+
result[index + 1] = value
|
|
166
|
+
return result
|
|
167
|
+
if text.startswith(f"{flag}="):
|
|
168
|
+
result = list(command)
|
|
169
|
+
result[index] = f"{flag}={value}"
|
|
170
|
+
return result
|
|
171
|
+
return [*command, flag, value]
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def parse_log_key_values(line: str) -> dict[str, str]:
|
|
175
|
+
"""Parse key=value pairs from a log line. Shared by all runners."""
|
|
176
|
+
import json
|
|
177
|
+
|
|
178
|
+
values: dict[str, str] = {}
|
|
179
|
+
pattern = re.compile(r'([A-Za-z_][A-Za-z0-9_.-]*)=("(?:\\.|[^"])*"|[^\s]+)')
|
|
180
|
+
for match in pattern.finditer(line):
|
|
181
|
+
raw = match.group(2)
|
|
182
|
+
if raw.startswith('"') and raw.endswith('"'):
|
|
183
|
+
try:
|
|
184
|
+
raw = json.loads(raw)
|
|
185
|
+
except json.JSONDecodeError:
|
|
186
|
+
raw = raw[1:-1]
|
|
187
|
+
values[match.group(1)] = str(raw)
|
|
188
|
+
return values
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def normalize_agent_value(value: str | None) -> str | None:
|
|
192
|
+
if value is None:
|
|
193
|
+
return None
|
|
194
|
+
text = str(value).strip()
|
|
195
|
+
if not text or text == "undefined":
|
|
196
|
+
return None
|
|
197
|
+
return text
|