@tonyclaw/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.
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