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.
- package/dist/src/adapters/providers/deepseek.js +3 -1
- package/dist/src/adapters/providers/deepseek.js.map +1 -1
- package/dist/src/adapters/providers/shared.js +3 -2
- package/dist/src/adapters/providers/shared.js.map +1 -1
- package/dist/src/backends/harbor/backend.js +19 -4
- package/dist/src/backends/harbor/backend.js.map +1 -1
- package/dist/src/cli/output.js +2 -0
- package/dist/src/cli/output.js.map +1 -1
- package/dist/src/evals/harbor-bridge-error.js +44 -0
- package/dist/src/evals/harbor-bridge-error.js.map +1 -0
- package/dist/src/evals/index.js +1 -1
- package/dist/src/evals/index.js.map +1 -1
- package/dist/src/evals/progress.js +3 -0
- package/dist/src/evals/progress.js.map +1 -1
- package/dist/src/evals/rerun-slots.js +95 -0
- package/dist/src/evals/rerun-slots.js.map +1 -0
- package/dist/src/evals/rerun.js +167 -148
- package/dist/src/evals/rerun.js.map +1 -1
- package/dist/src/evals/service.js +141 -84
- package/dist/src/evals/service.js.map +1 -1
- package/dist/src/evals/trial-import.js +20 -4
- package/dist/src/evals/trial-import.js.map +1 -1
- package/integrations/harbor/hitch_harbor_agent.py +262 -34
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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((
|
|
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
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
-
|
|
959
|
-
|
|
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
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
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
|
-
|
|
1000
|
-
raise
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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)))
|