agent-hitch 0.2.3 → 0.2.5

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.
Files changed (31) hide show
  1. package/dist/src/adapters/providers/deepseek.js +3 -1
  2. package/dist/src/adapters/providers/deepseek.js.map +1 -1
  3. package/dist/src/adapters/providers/shared.js +3 -2
  4. package/dist/src/adapters/providers/shared.js.map +1 -1
  5. package/dist/src/backends/harbor/backend.js +100 -36
  6. package/dist/src/backends/harbor/backend.js.map +1 -1
  7. package/dist/src/backends/harbor/dataset-config.js +36 -0
  8. package/dist/src/backends/harbor/dataset-config.js.map +1 -0
  9. package/dist/src/cli/commands/eval.js +39 -3
  10. package/dist/src/cli/commands/eval.js.map +1 -1
  11. package/dist/src/cli/output.js +4 -1
  12. package/dist/src/cli/output.js.map +1 -1
  13. package/dist/src/evals/harbor-bridge-error.js +44 -0
  14. package/dist/src/evals/harbor-bridge-error.js.map +1 -0
  15. package/dist/src/evals/index.js +4 -2
  16. package/dist/src/evals/index.js.map +1 -1
  17. package/dist/src/evals/progress.js +198 -0
  18. package/dist/src/evals/progress.js.map +1 -0
  19. package/dist/src/evals/request.js +37 -1
  20. package/dist/src/evals/request.js.map +1 -1
  21. package/dist/src/evals/rerun-slots.js +95 -0
  22. package/dist/src/evals/rerun-slots.js.map +1 -0
  23. package/dist/src/evals/rerun.js +417 -0
  24. package/dist/src/evals/rerun.js.map +1 -0
  25. package/dist/src/evals/service.js +193 -44
  26. package/dist/src/evals/service.js.map +1 -1
  27. package/dist/src/evals/trial-import.js +130 -60
  28. package/dist/src/evals/trial-import.js.map +1 -1
  29. package/docs/schemas/eval-progress.schema.json +61 -0
  30. package/integrations/harbor/hitch_harbor_agent.py +270 -30
  31. package/package.json +1 -1
@@ -13,7 +13,7 @@ import shutil
13
13
  import stat as stat_module
14
14
  import tempfile
15
15
  import uuid
16
- from datetime import datetime
16
+ from datetime import datetime, timezone
17
17
  from pathlib import Path, PurePosixPath
18
18
  from typing import Any
19
19
 
@@ -28,6 +28,20 @@ LOCAL_GIT_TRANSPORT_MAX_BYTES = 512 * 1024 * 1024
28
28
  LOCAL_GIT_REMOTE_ROOT = "/opt/hitch-local-source"
29
29
  HARNESS_ARTIFACT_REMOTE_ROOT = "/opt/hitch-harness-artifact"
30
30
  HITCH_CONTAINER_STATE_ROOT = "/tmp/hitch-state"
31
+ HITCH_BRIDGE_ERROR_LOG = "/logs/agent/hitch-bridge-error.json"
32
+ HITCH_DIAGNOSTIC_MAX_BYTES = 8 * 1024
33
+ HITCH_BRIDGE_ERROR_MAX_BYTES = 64 * 1024
34
+ HITCH_RESULT_MISSING_EXIT = 44
35
+ HITCH_RESULT_NOT_FILE_EXIT = 45
36
+
37
+
38
+ class HitchBridgeError(RuntimeError):
39
+ """Stable Harbor-facing infrastructure failure with structured evidence."""
40
+
41
+ def __init__(self, code: str, message: str, evidence: dict[str, Any]) -> None:
42
+ super().__init__(f"{code}: {message}")
43
+ self.code = code
44
+ self.evidence = evidence
31
45
 
32
46
 
33
47
  class HitchHarborAgent(BaseAgent):
@@ -51,6 +65,7 @@ class HitchHarborAgent(BaseAgent):
51
65
  benchmark_id: str | None = None,
52
66
  benchmark_revision: str | None = None,
53
67
  verifier_identity: str | None = None,
68
+ logical_attempt: int | None = None,
54
69
  **kwargs: Any,
55
70
  ) -> None:
56
71
  super().__init__(logs_dir=logs_dir, **kwargs)
@@ -69,6 +84,9 @@ class HitchHarborAgent(BaseAgent):
69
84
  self.benchmark_id = benchmark_id
70
85
  self.benchmark_revision = benchmark_revision
71
86
  self.verifier_identity = verifier_identity
87
+ if logical_attempt is not None and (isinstance(logical_attempt, bool) or not isinstance(logical_attempt, int) or logical_attempt < 1):
88
+ raise ValueError("logical_attempt must be a positive integer")
89
+ self.logical_attempt = logical_attempt
72
90
  self._hitch_version: str | None = None
73
91
  self._entrypoint: str | None = None
74
92
  self._artifact_manifest: dict[str, Any] | None = None
@@ -816,7 +834,8 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
816
834
  context: AgentContext,
817
835
  ) -> None:
818
836
  self.logs_dir.mkdir(parents=True, exist_ok=True)
819
- run_id = "run_" + uuid.uuid4().hex
837
+ assigned_run_id = "run_" + uuid.uuid4().hex
838
+ run_id = assigned_run_id
820
839
  trial_id, task_id, attempt = self._trial_identity()
821
840
  context_payload: dict[str, Any] = {"kind": "ad_hoc"}
822
841
  parent_payload: dict[str, Any] | None = None
@@ -920,31 +939,82 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
920
939
  )
921
940
  execution = await environment.exec(command, cwd=self.workdir)
922
941
  events = self._events(execution.stdout or "")
923
- observed_run_id = next((event.get("run_id") for event in events if event.get("run_id")), None)
942
+ observed_run_id = next((
943
+ value
944
+ for event in events
945
+ for value in [event.get("run_id")]
946
+ if isinstance(value, str) and re.fullmatch(r"run_[a-f0-9]{32}", value)
947
+ ), None)
924
948
  if observed_run_id:
925
949
  run_id = str(observed_run_id)
926
- hitch_result = None
927
- if run_id:
928
- result_path = f"/tmp/hitch-state/runs/{run_id}/result.json"
929
- result = await environment.exec(
930
- f"cat {shlex.quote(result_path)} | tee /logs/agent/hitch-result.json"
931
- )
932
- if result.return_code == 0 and result.stdout:
933
- hitch_result = json.loads(result.stdout)
934
- export = await environment.exec(
935
- f"""
950
+ result_path = f"/tmp/hitch-state/runs/{run_id}/result.json"
951
+ quoted_result_path = shlex.quote(result_path)
952
+ result_read = await environment.exec(
953
+ f"""
954
+ if [ ! -e {quoted_result_path} ]; then exit {HITCH_RESULT_MISSING_EXIT}; fi
955
+ if [ -L {quoted_result_path} ] || [ ! -f {quoted_result_path} ]; then exit {HITCH_RESULT_NOT_FILE_EXIT}; fi
956
+ cat -- {quoted_result_path}
957
+ """.strip()
958
+ )
959
+ result_copy = await environment.exec(
960
+ f"if [ -f {quoted_result_path} ] && [ ! -L {quoted_result_path} ]; then "
961
+ f"cp -- {quoted_result_path} /logs/agent/hitch-result.json; fi"
962
+ )
963
+ bundle_stage = f"/logs/agent/.hitch-run-bundle.{uuid.uuid4().hex}"
964
+ bundle_marker = json.dumps({
965
+ "schema_version": "1",
966
+ "run_id": run_id,
967
+ "eval_id": self.eval_id,
968
+ "trial_id": trial_id,
969
+ "completed_at": datetime.now(timezone.utc).isoformat(),
970
+ }, separators=(",", ":"))
971
+ bundle_export = await environment.exec(
972
+ f"""
936
973
  set -eu
937
974
  source_dir={shlex.quote(f'/tmp/hitch-state/runs/{run_id}')}
938
975
  target_dir=/logs/agent/hitch-run-bundle
939
- rm -rf "$target_dir"
940
- mkdir -p "$target_dir"
976
+ stage_dir={shlex.quote(bundle_stage)}
977
+ rm -rf "$stage_dir"
978
+ mkdir -p "$stage_dir"
941
979
  for name in request.json resolution.json manifest.json result.json events.jsonl stdout.log stderr.log trajectory.ref.json trajectory; do
942
- if [ -e "$source_dir/$name" ]; then cp -a "$source_dir/$name" "$target_dir/$name"; fi
980
+ if [ -e "$source_dir/$name" ]; then cp -a "$source_dir/$name" "$stage_dir/$name"; fi
943
981
  done
982
+ printf '%s\n' {shlex.quote(bundle_marker)} > "$stage_dir/bundle.complete.json"
983
+ rm -rf "$target_dir"
984
+ mv "$stage_dir" "$target_dir"
944
985
  """.strip()
986
+ )
987
+ hitch_result, result_error_code, result_error_message = self._parse_hitch_result(result_read, run_id)
988
+ primary_code: str | None = None
989
+ primary_message: str | None = None
990
+ if execution.return_code != 0:
991
+ primary_code = "hitch_process_failed"
992
+ diagnostic = (execution.stderr or "").strip()
993
+ if hitch_result and isinstance(hitch_result.get("error"), dict):
994
+ result_message = hitch_result["error"].get("message")
995
+ if isinstance(result_message, str) and result_message.strip():
996
+ diagnostic = result_message.strip()
997
+ primary_message = (
998
+ f"Hitch agent run failed with code {execution.return_code} "
999
+ f"(run_id={run_id}, trial_id={trial_id}): {self._bounded_tail(diagnostic or 'no diagnostic output')}"
945
1000
  )
946
- if export.return_code != 0:
947
- raise RuntimeError("Hitch run bundle export failed before trial teardown")
1001
+ elif result_error_code is not None:
1002
+ primary_code = result_error_code
1003
+ primary_message = result_error_message
1004
+ elif hitch_result is not None and hitch_result.get("revision_identity") != self.revision_identity:
1005
+ primary_code = "hitch_revision_identity_mismatch"
1006
+ primary_message = (
1007
+ "Hitch resolved a different harness revision inside the trial container "
1008
+ f"(run_id={run_id}, trial_id={trial_id}): expected {self.revision_identity}, "
1009
+ f"got {hitch_result.get('revision_identity')}"
1010
+ )
1011
+ elif bundle_export.return_code != 0:
1012
+ primary_code = "hitch_run_bundle_export_failed"
1013
+ primary_message = f"Hitch run bundle export failed (run_id={run_id}, trial_id={trial_id})"
1014
+ elif result_copy.return_code != 0:
1015
+ primary_code = "hitch_result_artifact_copy_failed"
1016
+ primary_message = f"Hitch result artifact copy failed (run_id={run_id}, trial_id={trial_id})"
1017
+
948
1018
  context.metadata = {
949
1019
  "candidate_id": self.candidate_id,
950
1020
  "harness_ref": self.harness_ref,
@@ -977,26 +1047,196 @@ done
977
1047
  "node_version": self._artifact_manifest.get("toolchain", {}).get("node"),
978
1048
  "status": self._artifact_transport_status or "container_prepare",
979
1049
  }
980
- if execution.return_code != 0:
981
- message = (execution.stderr or "").strip()
982
- if hitch_result and hitch_result.get("error", {}).get("message"):
983
- message = hitch_result["error"]["message"]
984
- raise RuntimeError(
985
- f"Hitch agent run failed with code {execution.return_code}: {message or 'no diagnostic output'}"
1050
+ if primary_code is not None:
1051
+ context.metadata["hitch_bridge_error_code"] = primary_code
1052
+ context.metadata["hitch_bridge_error_artifact"] = "hitch-bridge-error.json"
1053
+ evidence = self._bridge_error_evidence(
1054
+ code=primary_code,
1055
+ message=primary_message or primary_code,
1056
+ trial_id=trial_id,
1057
+ task_id=task_id,
1058
+ attempt=attempt,
1059
+ assigned_run_id=assigned_run_id,
1060
+ observed_run_id=str(observed_run_id) if observed_run_id else None,
1061
+ result_path=result_path,
1062
+ execution=execution,
1063
+ result_read=result_read,
1064
+ result_diagnostic=result_error_code,
1065
+ last_event=events[-1] if events else None,
1066
+ bundle_export=bundle_export,
1067
+ result_copy=result_copy,
986
1068
  )
987
- if hitch_result is None:
988
- raise RuntimeError("Hitch agent run completed without a persisted result")
989
- if hitch_result.get("revision_identity") != self.revision_identity:
990
- raise RuntimeError(
991
- "Hitch resolved a different harness revision inside the trial container: "
992
- f"expected {self.revision_identity}, got {hitch_result.get('revision_identity')}"
1069
+ await self._write_bridge_error(environment, evidence)
1070
+ raise HitchBridgeError(primary_code, primary_message or primary_code, evidence)
1071
+
1072
+ @staticmethod
1073
+ def _parse_hitch_result(
1074
+ result: ExecResult,
1075
+ expected_run_id: str,
1076
+ ) -> tuple[dict[str, Any] | None, str | None, str | None]:
1077
+ if result.return_code != 0:
1078
+ if result.return_code == HITCH_RESULT_MISSING_EXIT:
1079
+ return None, "hitch_result_missing", f"Hitch result file is missing (run_id={expected_run_id})"
1080
+ if result.return_code == HITCH_RESULT_NOT_FILE_EXIT:
1081
+ return None, "hitch_result_not_file", f"Hitch result path is not a regular file (run_id={expected_run_id})"
1082
+ return None, "hitch_result_read_failed", (
1083
+ f"Hitch result file could not be read with code {result.return_code} (run_id={expected_run_id})"
1084
+ )
1085
+ payload = (result.stdout or "").strip()
1086
+ if not payload:
1087
+ return None, "hitch_result_empty", f"Hitch result file is empty (run_id={expected_run_id})"
1088
+ try:
1089
+ value = json.loads(payload)
1090
+ except json.JSONDecodeError:
1091
+ return None, "hitch_result_invalid_json", f"Hitch result is not valid JSON (run_id={expected_run_id})"
1092
+ if not isinstance(value, dict):
1093
+ return None, "hitch_result_schema_invalid", f"Hitch result must be a JSON object (run_id={expected_run_id})"
1094
+ error = HitchHarborAgent._result_schema_error(value)
1095
+ if error is not None:
1096
+ return None, "hitch_result_schema_invalid", f"Hitch result schema is invalid: {error} (run_id={expected_run_id})"
1097
+ if value["run_id"] != expected_run_id:
1098
+ return None, "hitch_result_run_id_mismatch", (
1099
+ f"Hitch result run id mismatch: expected {expected_run_id}, got {value['run_id']}"
993
1100
  )
1101
+ return value, None, None
1102
+
1103
+ @staticmethod
1104
+ def _result_schema_error(value: dict[str, Any]) -> str | None:
1105
+ if value.get("schema_version") != "1":
1106
+ return "schema_version must be '1'"
1107
+ run_id = value.get("run_id")
1108
+ if not isinstance(run_id, str) or re.fullmatch(r"run_[a-f0-9]{32}", run_id) is None:
1109
+ return "run_id is invalid"
1110
+ if value.get("status") not in {"succeeded", "failed", "timed_out", "cancelled"}:
1111
+ return "status is invalid"
1112
+ exit_code = value.get("exit_code")
1113
+ if isinstance(exit_code, bool) or not isinstance(exit_code, int) or exit_code < 0:
1114
+ return "exit_code must be a non-negative integer"
1115
+ completed_at = value.get("completed_at")
1116
+ if not isinstance(completed_at, str) or not completed_at.strip():
1117
+ return "completed_at must be a non-empty string"
1118
+ if "error" in value:
1119
+ error = value["error"]
1120
+ if not isinstance(error, dict):
1121
+ return "error must be an object"
1122
+ if not isinstance(error.get("code"), str) or not isinstance(error.get("message"), str):
1123
+ return "error.code and error.message must be strings"
1124
+ return None
1125
+
1126
+ def _bridge_error_evidence(
1127
+ self,
1128
+ *,
1129
+ code: str,
1130
+ message: str,
1131
+ trial_id: str,
1132
+ task_id: str,
1133
+ attempt: int,
1134
+ assigned_run_id: str,
1135
+ observed_run_id: str | None,
1136
+ result_path: str,
1137
+ execution: ExecResult,
1138
+ result_read: ExecResult,
1139
+ result_diagnostic: str | None,
1140
+ last_event: dict[str, Any] | None,
1141
+ bundle_export: ExecResult,
1142
+ result_copy: ExecResult,
1143
+ ) -> dict[str, Any]:
1144
+ signal_value = getattr(execution, "signal", None)
1145
+ evidence: dict[str, Any] = {
1146
+ "schema_version": "1",
1147
+ "code": code,
1148
+ "message": self._bounded_tail(message, 2048),
1149
+ "recorded_at": datetime.now(timezone.utc).isoformat(),
1150
+ "eval_id": self.eval_id,
1151
+ "trial_id": trial_id,
1152
+ "task_id": task_id,
1153
+ "attempt": attempt,
1154
+ "assigned_run_id": assigned_run_id,
1155
+ "observed_run_id": observed_run_id,
1156
+ "result_path": result_path,
1157
+ "process": {
1158
+ "return_code": execution.return_code,
1159
+ "signal": signal_value if isinstance(signal_value, str) else None,
1160
+ "stdout_tail": self._bounded_tail(execution.stdout or ""),
1161
+ "stderr_tail": self._bounded_tail(execution.stderr or ""),
1162
+ },
1163
+ "result_read": {
1164
+ "return_code": result_read.return_code,
1165
+ "stdout_tail": self._bounded_tail(result_read.stdout or ""),
1166
+ "stderr_tail": self._bounded_tail(result_read.stderr or ""),
1167
+ },
1168
+ "last_event": self._bounded_event(last_event),
1169
+ "result_diagnostic": result_diagnostic,
1170
+ }
1171
+ if bundle_export.return_code != 0:
1172
+ evidence["bundle_export"] = {
1173
+ "return_code": bundle_export.return_code,
1174
+ "stdout_tail": self._bounded_tail(bundle_export.stdout or ""),
1175
+ "stderr_tail": self._bounded_tail(bundle_export.stderr or ""),
1176
+ }
1177
+ if result_copy.return_code != 0:
1178
+ evidence["result_copy"] = {
1179
+ "return_code": result_copy.return_code,
1180
+ "stdout_tail": self._bounded_tail(result_copy.stdout or ""),
1181
+ "stderr_tail": self._bounded_tail(result_copy.stderr or ""),
1182
+ }
1183
+ if len(json.dumps(evidence, ensure_ascii=False, separators=(",", ":")).encode("utf-8")) > HITCH_BRIDGE_ERROR_MAX_BYTES:
1184
+ evidence["message"] = self._bounded_tail(str(evidence["message"]), 1024)
1185
+ evidence["last_event"] = self._bounded_event_summary(last_event)
1186
+ for section_name in ["process", "result_read", "bundle_export", "result_copy"]:
1187
+ section = evidence.get(section_name)
1188
+ if not isinstance(section, dict):
1189
+ continue
1190
+ for field in ["stdout_tail", "stderr_tail"]:
1191
+ section[field] = self._bounded_tail(str(section.get(field, "")), 2048)
1192
+ evidence["diagnostics_truncated"] = True
1193
+ return evidence
1194
+
1195
+ @staticmethod
1196
+ def _bounded_tail(value: str, max_bytes: int = HITCH_DIAGNOSTIC_MAX_BYTES) -> str:
1197
+ encoded = value.encode("utf-8", errors="replace")
1198
+ if len(encoded) <= max_bytes:
1199
+ return value
1200
+ marker = f"[truncated {len(encoded) - max_bytes} bytes]\n".encode("utf-8")
1201
+ available = max(0, max_bytes - len(marker))
1202
+ suffix = encoded[-available:] if available else b""
1203
+ suffix = suffix.decode("utf-8", errors="ignore").encode("utf-8")
1204
+ return (marker + suffix).decode("utf-8")
1205
+
1206
+ @staticmethod
1207
+ def _bounded_event(event: dict[str, Any] | None) -> dict[str, Any] | None:
1208
+ if event is None:
1209
+ return None
1210
+ encoded = json.dumps(event, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
1211
+ if len(encoded) <= HITCH_DIAGNOSTIC_MAX_BYTES:
1212
+ return event
1213
+ return HitchHarborAgent._bounded_event_summary(event)
1214
+
1215
+ @staticmethod
1216
+ def _bounded_event_summary(event: dict[str, Any] | None) -> dict[str, Any] | None:
1217
+ if event is None:
1218
+ return None
1219
+ return {
1220
+ "type": event.get("type"),
1221
+ "run_id": event.get("run_id"),
1222
+ "truncated": True,
1223
+ }
1224
+
1225
+ @staticmethod
1226
+ async def _write_bridge_error(environment: BaseEnvironment, evidence: dict[str, Any]) -> None:
1227
+ payload = json.dumps(evidence, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
1228
+ await environment.exec(
1229
+ f"umask 077; printf '%s\\n' {shlex.quote(payload)} > {HITCH_BRIDGE_ERROR_LOG}"
1230
+ )
994
1231
 
995
1232
  def _trial_identity(self) -> tuple[str, str, int]:
996
1233
  """Read Harbor's stable trial/task identity from the persisted trial state."""
997
1234
  trial_dir = self.logs_dir.parent if self.logs_dir.name == "agent" else self.logs_dir
998
1235
  trial_id = trial_dir.name or "trial__1"
999
1236
  task_id = self._locked_task_id(trial_dir)
1237
+ if self.logical_attempt is not None:
1238
+ fallback_task_id = trial_id.rsplit("__", 1)[0] if "__" in trial_id else trial_id
1239
+ return trial_id, task_id or fallback_task_id, self.logical_attempt
1000
1240
  match = re.fullmatch(r"(.+)__(\d+)", trial_id)
1001
1241
  if match:
1002
1242
  return trial_id, task_id or match.group(1), max(1, int(match.group(2)))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hitch",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Content-addressed version control and evidence storage for agent harnesses",
5
5
  "keywords": [
6
6
  "ai-agents",