agent-hitch 0.2.4 → 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.
@@ -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,27 +939,37 @@ 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
- bundle_stage = f"/logs/agent/.hitch-run-bundle.{uuid.uuid4().hex}"
935
- bundle_marker = json.dumps({
936
- "schema_version": "1",
937
- "run_id": run_id,
938
- "eval_id": self.eval_id,
939
- "trial_id": trial_id,
940
- "completed_at": datetime.now(timezone.utc).isoformat(),
941
- }, separators=(",", ":"))
942
- export = await environment.exec(
943
- 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"""
944
973
  set -eu
945
974
  source_dir={shlex.quote(f'/tmp/hitch-state/runs/{run_id}')}
946
975
  target_dir=/logs/agent/hitch-run-bundle
@@ -954,9 +983,38 @@ printf '%s\n' {shlex.quote(bundle_marker)} > "$stage_dir/bundle.complete.json"
954
983
  rm -rf "$target_dir"
955
984
  mv "$stage_dir" "$target_dir"
956
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')}"
957
1000
  )
958
- if export.return_code != 0:
959
- 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
+
960
1018
  context.metadata = {
961
1019
  "candidate_id": self.candidate_id,
962
1020
  "harness_ref": self.harness_ref,
@@ -989,26 +1047,196 @@ mv "$stage_dir" "$target_dir"
989
1047
  "node_version": self._artifact_manifest.get("toolchain", {}).get("node"),
990
1048
  "status": self._artifact_transport_status or "container_prepare",
991
1049
  }
992
- if execution.return_code != 0:
993
- message = (execution.stderr or "").strip()
994
- if hitch_result and hitch_result.get("error", {}).get("message"):
995
- message = hitch_result["error"]["message"]
996
- raise RuntimeError(
997
- 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,
998
1068
  )
999
- if hitch_result is None:
1000
- raise RuntimeError("Hitch agent run completed without a persisted result")
1001
- if hitch_result.get("revision_identity") != self.revision_identity:
1002
- raise RuntimeError(
1003
- "Hitch resolved a different harness revision inside the trial container: "
1004
- 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']}"
1005
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
+ )
1006
1231
 
1007
1232
  def _trial_identity(self) -> tuple[str, str, int]:
1008
1233
  """Read Harbor's stable trial/task identity from the persisted trial state."""
1009
1234
  trial_dir = self.logs_dir.parent if self.logs_dir.name == "agent" else self.logs_dir
1010
1235
  trial_id = trial_dir.name or "trial__1"
1011
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
1012
1240
  match = re.fullmatch(r"(.+)__(\d+)", trial_id)
1013
1241
  if match:
1014
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.4",
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",