hivekit 0.8.0__tar.gz → 0.8.2__tar.gz
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.
- {hivekit-0.8.0 → hivekit-0.8.2}/PKG-INFO +2 -1
- {hivekit-0.8.0 → hivekit-0.8.2}/pyproject.toml +2 -1
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/config.py +69 -73
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/experiment.py +0 -2
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/main.py +60 -4
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/skills/hive-setup/SKILL.md +9 -1
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/skills/hive-setup/references/configuration.md +20 -0
- hivekit-0.8.2/src/cli/update_check.py +136 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/archive.py +35 -2
- hivekit-0.8.2/src/cli/utils/attachments.py +225 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/config_paths.py +0 -8
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/upload.py +19 -9
- {hivekit-0.8.0 → hivekit-0.8.2}/src/hivekit.egg-info/PKG-INFO +2 -1
- {hivekit-0.8.0 → hivekit-0.8.2}/src/hivekit.egg-info/SOURCES.txt +4 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/hivekit.egg-info/requires.txt +1 -0
- hivekit-0.8.2/tests/test_attachments.py +171 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_config.py +209 -116
- {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_experiment.py +8 -1
- {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_main.py +293 -0
- hivekit-0.8.2/tests/test_update_check.py +220 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/LICENSE +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/README.md +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/setup.cfg +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/__init__.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/__init__.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/auth_utils.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/credential_store.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/login_page.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/logo.svg +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/oidc_flow.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/session_manager.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/auth/token_revoker.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/completers.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/http_client.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/skills/hive-setup/references/gpu-hardware.md +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/skills/hive-setup/references/multi-evaluator.md +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/skills_install.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/__init__.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/config_sync.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/docker.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/logger.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/terminal.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/time.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/utils/url_utils.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/cli/version.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/hivekit.egg-info/dependency_links.txt +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/hivekit.egg-info/entry_points.txt +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/src/hivekit.egg-info/top_level.txt +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_completers.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_config_sync.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_http_client.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_login.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_logout.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_overrides.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_push_image.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_time_utils.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_upload.py +0 -0
- {hivekit-0.8.0 → hivekit-0.8.2}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hivekit
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
4
4
|
Summary: Toolkits for using hive agents, including CLI and SDK.
|
|
5
5
|
License:
|
|
6
6
|
Apache License
|
|
@@ -210,6 +210,7 @@ Requires-Python: >=3.12
|
|
|
210
210
|
Description-Content-Type: text/markdown
|
|
211
211
|
License-File: LICENSE
|
|
212
212
|
Requires-Dist: PyYAML>=5.1
|
|
213
|
+
Requires-Dist: packaging>=21.0
|
|
213
214
|
Requires-Dist: ruamel-yaml>=0.18
|
|
214
215
|
Requires-Dist: pydantic>=1.8.2
|
|
215
216
|
Requires-Dist: gitpython>=3.1.58
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hivekit"
|
|
7
|
-
version = "0.8.
|
|
7
|
+
version = "0.8.2"
|
|
8
8
|
description = "Toolkits for using hive agents, including CLI and SDK."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -12,6 +12,7 @@ license = { file = "LICENSE" }
|
|
|
12
12
|
|
|
13
13
|
dependencies = [
|
|
14
14
|
"PyYAML>=5.1",
|
|
15
|
+
"packaging>=21.0",
|
|
15
16
|
"ruamel-yaml>=0.18",
|
|
16
17
|
"pydantic>=1.8.2",
|
|
17
18
|
"gitpython>=3.1.58",
|
|
@@ -11,6 +11,7 @@ import pydantic
|
|
|
11
11
|
import yaml
|
|
12
12
|
from kubernetes.utils import parse_quantity
|
|
13
13
|
from pydantic import (
|
|
14
|
+
AnyHttpUrl,
|
|
14
15
|
ConfigDict,
|
|
15
16
|
Field,
|
|
16
17
|
PrivateAttr,
|
|
@@ -19,7 +20,6 @@ from pydantic import (
|
|
|
19
20
|
model_validator,
|
|
20
21
|
)
|
|
21
22
|
|
|
22
|
-
from cli.utils.config_paths import get_coordinator_overlay_path
|
|
23
23
|
from cli.utils.logger import set_log_level
|
|
24
24
|
from cli.utils.time import unique_suffix
|
|
25
25
|
|
|
@@ -256,11 +256,67 @@ class PromptConfig(BaseModel):
|
|
|
256
256
|
default=None,
|
|
257
257
|
description="A list of ideas which will be randomly sampled to inject into the Hive.",
|
|
258
258
|
)
|
|
259
|
-
|
|
259
|
+
attachments: Optional[list[str]] = Field(
|
|
260
260
|
default=None,
|
|
261
|
-
description=
|
|
261
|
+
description=(
|
|
262
|
+
"A list of local PDF files/directories or HTTP/HTTPS PDF URLs to provide as "
|
|
263
|
+
"reference material. Directories are searched recursively for files with a "
|
|
264
|
+
".pdf extension (case-insensitive). Local relative paths resolve from the "
|
|
265
|
+
"directory where hive is run."
|
|
266
|
+
),
|
|
262
267
|
)
|
|
263
268
|
|
|
269
|
+
@model_validator(mode="before")
|
|
270
|
+
@classmethod
|
|
271
|
+
def migrate_arxiv_ids(cls, values):
|
|
272
|
+
"""Convert legacy IDs into attachment URLs without mutating the input."""
|
|
273
|
+
if not isinstance(values, dict) or "arxiv_ids" not in values:
|
|
274
|
+
return values
|
|
275
|
+
values = dict(values)
|
|
276
|
+
# Preserve the legacy list's type coercion while folding it into attachments.
|
|
277
|
+
string_list = pydantic.TypeAdapter(Optional[list[str]], config=cls.model_config)
|
|
278
|
+
legacy = string_list.validate_python(values.pop("arxiv_ids"))
|
|
279
|
+
converted_urls = [f"https://arxiv.org/pdf/{arxiv_id}" for arxiv_id in legacy or []]
|
|
280
|
+
if converted_urls:
|
|
281
|
+
attachments = string_list.validate_python(
|
|
282
|
+
cls.validate_attachments_list(values.get("attachments"))
|
|
283
|
+
)
|
|
284
|
+
values["attachments"] = [*(attachments or []), *converted_urls]
|
|
285
|
+
warning = (
|
|
286
|
+
"prompt.arxiv_ids is deprecated and is converted to PDF URLs in "
|
|
287
|
+
"prompt.attachments. Use PDF URLs in prompt.attachments and remove "
|
|
288
|
+
"prompt.arxiv_ids to silence this warning."
|
|
289
|
+
)
|
|
290
|
+
if converted_urls:
|
|
291
|
+
warning += "\nConverted PDF URLs:\n" + "\n".join(f" - {url}" for url in converted_urls)
|
|
292
|
+
logger.warning(warning)
|
|
293
|
+
return values
|
|
294
|
+
|
|
295
|
+
@field_validator("attachments", mode="before")
|
|
296
|
+
@classmethod
|
|
297
|
+
def validate_attachments_list(cls, value):
|
|
298
|
+
if isinstance(value, str):
|
|
299
|
+
raise ValueError(
|
|
300
|
+
"Must be a YAML list, not a plain string. Example:\n attachments:\n - paper.pdf"
|
|
301
|
+
)
|
|
302
|
+
return value
|
|
303
|
+
|
|
304
|
+
@field_validator("attachments")
|
|
305
|
+
@classmethod
|
|
306
|
+
def validate_attachment_syntax(cls, value: Optional[list[str]]) -> Optional[list[str]]:
|
|
307
|
+
"""Check attachment syntax without accessing local files or the network."""
|
|
308
|
+
if not value:
|
|
309
|
+
return value
|
|
310
|
+
http_url = pydantic.TypeAdapter(AnyHttpUrl)
|
|
311
|
+
for source in value:
|
|
312
|
+
if not source.strip():
|
|
313
|
+
raise ValueError("Attachment paths and URLs must not be empty")
|
|
314
|
+
# Recognize URL prefixes without treating colons in local filenames as schemes.
|
|
315
|
+
if re.match(r"(?i)^(?:[a-z][a-z0-9+.-]*://|https?:)", source.lstrip()):
|
|
316
|
+
http_url.validate_python(source, strict=True)
|
|
317
|
+
# Keep configured paths and URLs unchanged, including URL query strings.
|
|
318
|
+
return value
|
|
319
|
+
|
|
264
320
|
|
|
265
321
|
def _extract_path(entry: str, strip_exclude: bool = False) -> str:
|
|
266
322
|
"""Reduce a file-list entry to the path it references."""
|
|
@@ -630,10 +686,9 @@ class HiveConfig(BaseModel):
|
|
|
630
686
|
coordinator_image_tag: Optional[str] = Field(
|
|
631
687
|
default=None,
|
|
632
688
|
description="The image tag to run the coordinator at, e.g. 'v0.6.0'. Optional — when "
|
|
633
|
-
"omitted,
|
|
634
|
-
"
|
|
635
|
-
"
|
|
636
|
-
"ignored in production environments.",
|
|
689
|
+
"omitted, the backend's default coordinator image is used. Set this in the experiment "
|
|
690
|
+
"config or with a command-line override. This field is ignored outside development "
|
|
691
|
+
"environments.",
|
|
637
692
|
)
|
|
638
693
|
runtime: RuntimeConfig = Field(
|
|
639
694
|
default_factory=RuntimeConfig, description="Runtime configuration for the experiment."
|
|
@@ -862,71 +917,12 @@ def _reconcile_num_sandboxes_override(cfg: dict, overridden_keys: set[tuple[str,
|
|
|
862
917
|
del runtime[loser]
|
|
863
918
|
|
|
864
919
|
|
|
865
|
-
def
|
|
866
|
-
"""
|
|
867
|
-
|
|
868
|
-
A broken overlay must not stop an experiment the user asked for, so anything
|
|
869
|
-
unusable is warned about and ignored. An absent, empty or non-scalar tag
|
|
870
|
-
records nothing; a bool is not a scalar here, being an int subclass.
|
|
871
|
-
"""
|
|
872
|
-
if not os.path.exists(overlay_path):
|
|
873
|
-
return None
|
|
874
|
-
try:
|
|
875
|
-
with open(overlay_path, "r") as file:
|
|
876
|
-
overlay = yaml.safe_load(file)
|
|
877
|
-
except (OSError, yaml.YAMLError) as e:
|
|
878
|
-
logger.warning("Ignoring %s, which could not be read: %s", overlay_path, e)
|
|
879
|
-
return None
|
|
880
|
-
tag = overlay.get("coordinator_image_tag") if isinstance(overlay, dict) else None
|
|
881
|
-
if tag is None or tag == "":
|
|
882
|
-
return None
|
|
883
|
-
if isinstance(tag, bool) or not isinstance(tag, (str, int, float)):
|
|
884
|
-
logger.warning(
|
|
885
|
-
"Ignoring coordinator_image_tag: %r in %s, which must be a string or number.",
|
|
886
|
-
tag,
|
|
887
|
-
overlay_path,
|
|
888
|
-
)
|
|
889
|
-
return None
|
|
890
|
-
return str(tag)
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
def _resolve_coordinator_image_tag(config_data: dict, overridden: bool) -> None:
|
|
894
|
-
"""Fall back to the coordinator image tag `hivectl generate config` recorded.
|
|
895
|
-
|
|
896
|
-
A tag the experiment asked for wins, even an empty one asking for the backend
|
|
897
|
-
default. Whichever tag is used is logged with where it came from, as is any
|
|
898
|
-
recorded tag it discards, so which coordinator image runs is never a surprise.
|
|
899
|
-
"""
|
|
900
|
-
overlay_path = get_coordinator_overlay_path()
|
|
901
|
-
recorded = _recorded_coordinator_image_tag(overlay_path)
|
|
902
|
-
if "coordinator_image_tag" not in config_data:
|
|
903
|
-
if recorded is None:
|
|
904
|
-
return
|
|
905
|
-
config_data["coordinator_image_tag"] = recorded
|
|
906
|
-
logger.warning(
|
|
907
|
-
"Using coordinator_image_tag=%s recorded in %s by `hivectl generate config`; "
|
|
908
|
-
"set coordinator_image_tag in the experiment config to override it.",
|
|
909
|
-
recorded,
|
|
910
|
-
overlay_path,
|
|
911
|
-
)
|
|
912
|
-
return
|
|
913
|
-
asked_for = config_data["coordinator_image_tag"]
|
|
920
|
+
def _log_coordinator_image_tag(config_data: dict, overridden: bool) -> None:
|
|
921
|
+
"""Report the tag explicitly selected by the experiment config or CLI."""
|
|
922
|
+
asked_for = config_data.get("coordinator_image_tag")
|
|
914
923
|
source = "a command-line override" if overridden else "the experiment config"
|
|
915
|
-
|
|
916
|
-
# an explicit null would match the tag "None" rather than discard it.
|
|
917
|
-
if recorded is not None and (asked_for is None or str(asked_for) != recorded):
|
|
918
|
-
logger.warning(
|
|
919
|
-
"Using coordinator_image_tag=%s from %s, discarding coordinator_image_tag=%s "
|
|
920
|
-
"recorded in %s.",
|
|
921
|
-
asked_for,
|
|
922
|
-
source,
|
|
923
|
-
recorded,
|
|
924
|
-
overlay_path,
|
|
925
|
-
)
|
|
926
|
-
elif asked_for is not None and asked_for != "":
|
|
924
|
+
if asked_for is not None and asked_for != "":
|
|
927
925
|
logger.warning("Using coordinator_image_tag=%s from %s.", asked_for, source)
|
|
928
|
-
# An empty tag with nothing to discard asks for the backend's default image,
|
|
929
|
-
# exactly as setting no tag at all does, so there is nothing to report.
|
|
930
926
|
|
|
931
927
|
|
|
932
928
|
def load_config(
|
|
@@ -936,8 +932,8 @@ def load_config(
|
|
|
936
932
|
) -> HiveConfig:
|
|
937
933
|
"""Load configuration from a YAML file.
|
|
938
934
|
|
|
939
|
-
Pass ``coordinator=False`` when no coordinator will run, so
|
|
940
|
-
coordinator image tag is
|
|
935
|
+
Pass ``coordinator=False`` when no coordinator will run, so an explicit
|
|
936
|
+
coordinator image tag is not reported as being used.
|
|
941
937
|
"""
|
|
942
938
|
with open(file_path, "r") as file:
|
|
943
939
|
raw_text = file.read()
|
|
@@ -948,7 +944,7 @@ def load_config(
|
|
|
948
944
|
overridden_keys = apply_overrides(config_data, overrides)
|
|
949
945
|
|
|
950
946
|
if coordinator and isinstance(config_data, dict):
|
|
951
|
-
|
|
947
|
+
_log_coordinator_image_tag(config_data, ("coordinator_image_tag",) in overridden_keys)
|
|
952
948
|
|
|
953
949
|
try:
|
|
954
950
|
config = HiveConfig(**config_data)
|
|
@@ -124,8 +124,6 @@ def build_experiment_crd(config: HiveConfig) -> Dict[str, Any]:
|
|
|
124
124
|
experiment["spec"]["prompt"]["context"] = config.prompt.context
|
|
125
125
|
if config.prompt.ideas:
|
|
126
126
|
experiment["spec"]["prompt"]["ideas"] = config.prompt.ideas
|
|
127
|
-
if config.prompt.arxiv_ids:
|
|
128
|
-
experiment["spec"]["prompt"]["arxivIds"] = config.prompt.arxiv_ids
|
|
129
127
|
if config.prompt.enable_evolution:
|
|
130
128
|
experiment["spec"]["prompt"]["enableEvolution"] = config.prompt.enable_evolution
|
|
131
129
|
|
|
@@ -11,6 +11,7 @@ import shlex
|
|
|
11
11
|
import sys
|
|
12
12
|
import tempfile
|
|
13
13
|
import time
|
|
14
|
+
import traceback
|
|
14
15
|
import warnings
|
|
15
16
|
import webbrowser
|
|
16
17
|
|
|
@@ -38,12 +39,14 @@ from cli.http_client import (
|
|
|
38
39
|
)
|
|
39
40
|
from cli.skills_install import TARGETS as SKILL_TARGETS
|
|
40
41
|
from cli.skills_install import skills_install
|
|
42
|
+
from cli.update_check import check_for_update, print_update_footer, warn_if_outdated
|
|
41
43
|
from cli.utils import docker
|
|
42
44
|
from cli.utils.archive import resolve_file_list
|
|
45
|
+
from cli.utils.attachments import AttachmentError, prepare_attachments
|
|
43
46
|
from cli.utils.config_paths import get_config_dir, get_config_path, load_organization_id
|
|
44
47
|
from cli.utils.config_sync import sync_base_image
|
|
45
48
|
from cli.utils.time import humanize_time
|
|
46
|
-
from cli.utils.upload import streaming_upload
|
|
49
|
+
from cli.utils.upload import streaming_upload, streaming_upload_entries
|
|
47
50
|
from cli.utils.url_utils import (
|
|
48
51
|
build_oidc_endpoints,
|
|
49
52
|
derive_identity_base_url,
|
|
@@ -251,6 +254,18 @@ def _upload_source_if_any(
|
|
|
251
254
|
return patch_id
|
|
252
255
|
|
|
253
256
|
|
|
257
|
+
def _upload_attachments_if_any(
|
|
258
|
+
client, attachments: list[str] | None, *, dry_run: bool = False
|
|
259
|
+
) -> str | None:
|
|
260
|
+
"""Prepare and upload PDFs, keeping downloads only for the upload's lifetime."""
|
|
261
|
+
with prepare_attachments(attachments or []) as entries:
|
|
262
|
+
if not entries or dry_run:
|
|
263
|
+
return None
|
|
264
|
+
resource_id = streaming_upload_entries(client, entries)
|
|
265
|
+
logger.debug("Resource upload complete. resource_id=%s", resource_id)
|
|
266
|
+
return resource_id
|
|
267
|
+
|
|
268
|
+
|
|
254
269
|
@contextlib.contextmanager
|
|
255
270
|
def _resolve_source(config):
|
|
256
271
|
"""Yield a local directory path for repo.source, cloning if remote."""
|
|
@@ -291,14 +306,24 @@ def create_experiment(args, overrides) -> None:
|
|
|
291
306
|
)
|
|
292
307
|
sys.exit(1)
|
|
293
308
|
|
|
309
|
+
attachments = config.prompt.attachments if config.prompt is not None else []
|
|
310
|
+
try:
|
|
311
|
+
resource_id = _upload_attachments_if_any(client, attachments, dry_run=args.dry_run)
|
|
312
|
+
except AttachmentError as e:
|
|
313
|
+
console.print(f"[bold red]Error:[/bold red] {e}", highlight=False)
|
|
314
|
+
sys.exit(1)
|
|
315
|
+
|
|
294
316
|
experiment_crd = experiment.build_experiment_crd(config)
|
|
295
317
|
|
|
296
318
|
patch_id = _upload_source_if_any(
|
|
297
319
|
client, config, dry_run=args.dry_run, allow_missing=args.allow_missing_files
|
|
298
320
|
)
|
|
299
|
-
if patch_id:
|
|
300
|
-
experiment_crd.setdefault("metadata", {}).setdefault("annotations", {})
|
|
301
|
-
|
|
321
|
+
if patch_id or resource_id:
|
|
322
|
+
annotations = experiment_crd.setdefault("metadata", {}).setdefault("annotations", {})
|
|
323
|
+
if patch_id:
|
|
324
|
+
annotations["hiverge.ai/patch-key"] = patch_id
|
|
325
|
+
if resource_id:
|
|
326
|
+
annotations["hiverge.ai/resource-key"] = resource_id
|
|
302
327
|
|
|
303
328
|
if args.dry_run:
|
|
304
329
|
console.print(
|
|
@@ -1626,6 +1651,27 @@ def main():
|
|
|
1626
1651
|
|
|
1627
1652
|
argcomplete.autocomplete(parser)
|
|
1628
1653
|
args, overrides = parser.parse_known_args()
|
|
1654
|
+
|
|
1655
|
+
status = check_for_update()
|
|
1656
|
+
if status is not None and status.outdated:
|
|
1657
|
+
warn_if_outdated(status)
|
|
1658
|
+
|
|
1659
|
+
try:
|
|
1660
|
+
_dispatch(parser, args, overrides)
|
|
1661
|
+
except SystemExit as exc:
|
|
1662
|
+
if status is not None and status.outdated and _is_failure_code(exc.code):
|
|
1663
|
+
print_update_footer(status)
|
|
1664
|
+
raise
|
|
1665
|
+
except Exception:
|
|
1666
|
+
if status is None or not status.outdated:
|
|
1667
|
+
raise
|
|
1668
|
+
traceback.print_exc()
|
|
1669
|
+
print_update_footer(status)
|
|
1670
|
+
raise SystemExit(1)
|
|
1671
|
+
|
|
1672
|
+
|
|
1673
|
+
def _dispatch(parser, args, overrides) -> None:
|
|
1674
|
+
"""Route parsed args to the selected command, or print help if none."""
|
|
1629
1675
|
if hasattr(args, "func"):
|
|
1630
1676
|
if args.func in (create_experiment, shell_command):
|
|
1631
1677
|
args.func(args, overrides)
|
|
@@ -1634,3 +1680,13 @@ def main():
|
|
|
1634
1680
|
args.func(args)
|
|
1635
1681
|
else:
|
|
1636
1682
|
parser.print_help()
|
|
1683
|
+
|
|
1684
|
+
|
|
1685
|
+
def _is_failure_code(code: object) -> bool:
|
|
1686
|
+
"""Whether a SystemExit code denotes a failure worth footing the notice on.
|
|
1687
|
+
|
|
1688
|
+
``0``/``None`` are success; 130 is a Ctrl-C interrupt, not a hive failure.
|
|
1689
|
+
Any other value (a nonzero int, or a message string ``sys.exit`` prints) is
|
|
1690
|
+
a failure.
|
|
1691
|
+
"""
|
|
1692
|
+
return code not in (0, None, 130)
|
|
@@ -307,7 +307,13 @@ This is the agents' primary steer. Write it as an onboarding document — imagin
|
|
|
307
307
|
|
|
308
308
|
- **Describe the problem, not the solution.** Do NOT suggest optimization approaches, likely bottlenecks, or directions to explore — that biases the Hive and narrows its search.
|
|
309
309
|
- **Never describe the current code.** This context is fixed while the target evolves every iteration, so anything about how the present implementation works is stale and misleading at once.
|
|
310
|
-
- **Distill useful background from the repo's README or design docs** into this prose —
|
|
310
|
+
- **Distill useful background from the repo's README or design docs** into this prose — `repo.additional_context` is for code files only. PDF references can be attached separately (see below).
|
|
311
|
+
|
|
312
|
+
### PDF reference material
|
|
313
|
+
|
|
314
|
+
When the user supplies research papers, technical manuals, or other PDF references, add them under `prompt.attachments`. They provide reference material for agents and are not included in candidate code. Keep the task and metric in `prompt.context`, with a short note on which references are relevant.
|
|
315
|
+
|
|
316
|
+
Use local PDF files, local directories, or PDF URLs. For arXiv papers, use the `/pdf/` URL, such as `https://arxiv.org/pdf/2605.10327`. See [PDF attachments](references/configuration.md#pdf-attachments) for an example, path handling, and limits.
|
|
311
317
|
|
|
312
318
|
### Key field notes
|
|
313
319
|
|
|
@@ -336,6 +342,8 @@ These drive cost and feasibility, so **when unsure about sandbox count, runtime,
|
|
|
336
342
|
|
|
337
343
|
After writing `hive.yaml`, you **must** run `hive create exp -c hive.yaml --dry-run` to confirm the config is valid. Fix any errors before moving on.
|
|
338
344
|
|
|
345
|
+
With `prompt.attachments`, the dry-run also checks local PDFs and downloads PDF URLs for validation. Relative attachment paths resolve from the directory where you run `hive`; use absolute paths if the launch directory may differ.
|
|
346
|
+
|
|
339
347
|
Add `--allow-missing-files` only when a `target_code`/`additional_context` path is deliberately absent from the upload — a prebuilt image with `repo.files` narrowed, `source: null`, or a file `setup_script` generates. It's accepted by `hive create exp` and `hive shell` alike; when the whole source directory is uploaded, omit it so a mistyped path still errors.
|
|
340
348
|
|
|
341
349
|
|
|
@@ -22,6 +22,7 @@ Source: https://docs.hiverge.ai/gettingstarted/cli/configuration
|
|
|
22
22
|
| `apiversion` | string | `v1alpha1` | Schema version |
|
|
23
23
|
| `experiment_name` | string | required | Valid DNS label (`[a-z0-9-]`, max 51 chars, no leading `-`); trailing `-` appends a random 7-char unique suffix, so a name ending in `-` may have at most 43 chars before the `-` |
|
|
24
24
|
| `coordinator_config_name` | string | `default-coordinator-config` | |
|
|
25
|
+
| `coordinator_image_tag` | string | `null` | Development environments only. Set explicitly here or via `coordinator_image_tag=<tag>` on the command line; omitted or empty uses the backend default. |
|
|
25
26
|
|
|
26
27
|
## `repo`
|
|
27
28
|
| Field | Type | Default | Notes |
|
|
@@ -108,6 +109,23 @@ Optional — omit for defaults. Steers the agents' search.
|
|
|
108
109
|
|---|---|---|---|
|
|
109
110
|
| `context` | string | — | Experiment-specific guidance, multi-line |
|
|
110
111
|
| `ideas` | list[string] | — | Distinct directions; one randomly sampled and injected each iteration |
|
|
112
|
+
| `attachments` | list[string] | `null` | Local PDF files, local directories, or PDF URLs. See [PDF attachments](#pdf-attachments). |
|
|
113
|
+
|
|
114
|
+
### PDF attachments
|
|
115
|
+
|
|
116
|
+
PDFs provide reference material for agents and are not included in candidate code.
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
prompt:
|
|
120
|
+
attachments:
|
|
121
|
+
- papers/reference.pdf
|
|
122
|
+
- background/
|
|
123
|
+
- https://arxiv.org/pdf/2605.10327
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- Relative paths resolve from the directory where you run `hive`, not from the location of `hive.yaml` or `repo.source`. Directories are searched recursively for PDFs.
|
|
127
|
+
- Each PDF can be up to **20 MiB**. URLs must return a PDF; use a paper's PDF download URL rather than its abstract or landing page.
|
|
128
|
+
- The CLI validates attachments before creating the experiment. `hive create exp -c hive.yaml --dry-run` also checks attachments, including downloading PDF URLs.
|
|
111
129
|
|
|
112
130
|
## Evaluator output contract
|
|
113
131
|
`evaluate.py` must print a JSON object on the **final line** of stdout, and must **exit 0 even when reporting a failure** — a non-zero exit code is a crashed evaluator, not a failed candidate. Report invalid candidates with `{"status": "failed", ...}` and exit cleanly.
|
|
@@ -181,6 +199,8 @@ prompt:
|
|
|
181
199
|
ideas:
|
|
182
200
|
- "Try batching database writes"
|
|
183
201
|
- "Consider async I/O for network calls"
|
|
202
|
+
attachments:
|
|
203
|
+
- https://arxiv.org/pdf/2605.10327
|
|
184
204
|
```
|
|
185
205
|
|
|
186
206
|
## CLI cheat-sheet
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Copyright (C) 2026 Hiverge
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Compare the running hivekit against the latest release on PyPI.
|
|
6
|
+
|
|
7
|
+
The check runs once at CLI startup. It is best-effort and fail-closed: any
|
|
8
|
+
network error, timeout, missing metadata, or unparseable version yields
|
|
9
|
+
an unknown status and skips the checks.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import tomllib
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
from packaging.version import InvalidVersion, Version
|
|
20
|
+
|
|
21
|
+
from cli.version import _direct_url, _editable_source_dir, _package_version
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("hivekit")
|
|
24
|
+
|
|
25
|
+
_PYPI_URL = "https://pypi.org/pypi/hivekit/json"
|
|
26
|
+
_TIMEOUT_SECONDS = 3.0
|
|
27
|
+
|
|
28
|
+
# Any non-empty value skips the network round-trip (CI, air-gapped, tests).
|
|
29
|
+
_SKIP_ENV = "HIVE_NO_UPDATE_CHECK"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Sits after the pinned install commands in both the warning and the footer.
|
|
33
|
+
_SKILLS_LINE = "then optionally update the Hive Skills with `hive skills install`."
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _upgrade_commands(latest: str) -> str:
|
|
37
|
+
"""The two pinned install commands, one per indented line.
|
|
38
|
+
|
|
39
|
+
The version pin (``hivekit==<latest>``) is required: a bare ``--upgrade
|
|
40
|
+
hivekit`` is a no-op whenever the resolver already considers the environment
|
|
41
|
+
satisfied, so the exact target version must be named.
|
|
42
|
+
"""
|
|
43
|
+
return (
|
|
44
|
+
f" uv pip install --upgrade hivekit=={latest}\n pip install --upgrade hivekit=={latest}"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class UpdateStatus:
|
|
50
|
+
local: str
|
|
51
|
+
latest: str
|
|
52
|
+
outdated: bool
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _local_version() -> str:
|
|
56
|
+
"""Version to compare against PyPI.
|
|
57
|
+
|
|
58
|
+
An editable install's recorded metadata is frozen at ``pip install -e``
|
|
59
|
+
time, so it lags the working tree. For a dev build the true version is
|
|
60
|
+
whatever the checkout's ``pyproject.toml`` declares now, so prefer that;
|
|
61
|
+
fall back to the recorded metadata for wheel/VCS/PyPI installs and whenever
|
|
62
|
+
the file cannot be read or declares no static version.
|
|
63
|
+
"""
|
|
64
|
+
source_dir = _editable_source_dir(_direct_url())
|
|
65
|
+
if source_dir is not None:
|
|
66
|
+
try:
|
|
67
|
+
with open(os.path.join(source_dir, "pyproject.toml"), "rb") as f:
|
|
68
|
+
version = tomllib.load(f)["project"]["version"]
|
|
69
|
+
logger.debug("Editable checkout: version %s from pyproject.toml", version)
|
|
70
|
+
return version
|
|
71
|
+
except (OSError, tomllib.TOMLDecodeError, KeyError, TypeError):
|
|
72
|
+
logger.debug("Editable checkout: pyproject.toml version unreadable", exc_info=True)
|
|
73
|
+
return _package_version()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _latest_version() -> str | None:
|
|
77
|
+
"""Return the newest hivekit version on PyPI, or ``None`` if unavailable."""
|
|
78
|
+
try:
|
|
79
|
+
resp = requests.get(_PYPI_URL, timeout=_TIMEOUT_SECONDS)
|
|
80
|
+
resp.raise_for_status()
|
|
81
|
+
latest = resp.json()["info"]["version"]
|
|
82
|
+
logger.debug("PyPI reports latest hivekit version %s", latest)
|
|
83
|
+
return latest
|
|
84
|
+
except Exception:
|
|
85
|
+
logger.debug("PyPI version check failed", exc_info=True)
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def check_for_update() -> UpdateStatus | None:
|
|
90
|
+
"""Compare the installed version against PyPI's latest.
|
|
91
|
+
|
|
92
|
+
Returns ``None`` when no comparison can be made (check disabled, network
|
|
93
|
+
failure, or either version unparseable); otherwise an ``UpdateStatus``.
|
|
94
|
+
"""
|
|
95
|
+
if os.getenv(_SKIP_ENV):
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
local = _local_version()
|
|
99
|
+
logger.debug("Installed hivekit version %s", local)
|
|
100
|
+
latest = _latest_version()
|
|
101
|
+
if latest is None:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
outdated = Version(local) < Version(latest)
|
|
106
|
+
except InvalidVersion:
|
|
107
|
+
logger.debug("Unparseable version (local=%r latest=%r)", local, latest)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
logger.debug("Version check: local=%s latest=%s outdated=%s", local, latest, outdated)
|
|
111
|
+
return UpdateStatus(local=local, latest=latest, outdated=outdated)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def warn_if_outdated(status: UpdateStatus) -> None:
|
|
115
|
+
"""Emit a startup WARNING when the running version is behind PyPI."""
|
|
116
|
+
logger.warning(
|
|
117
|
+
"hivekit %s is out of date; the latest release is %s. Update with:\n%s\n%s",
|
|
118
|
+
status.local,
|
|
119
|
+
status.latest,
|
|
120
|
+
_upgrade_commands(status.latest),
|
|
121
|
+
_SKILLS_LINE,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def print_update_footer(status: UpdateStatus) -> None:
|
|
126
|
+
"""Restate the staleness as the final lines after a failed command.
|
|
127
|
+
|
|
128
|
+
Written to stderr so it trails the traceback or error a failure already
|
|
129
|
+
emitted there.
|
|
130
|
+
"""
|
|
131
|
+
print(
|
|
132
|
+
f"\nThe current version of hivekit is {status.local} the latest version "
|
|
133
|
+
f"is {status.latest}. Please update with:\n{_upgrade_commands(status.latest)}\n"
|
|
134
|
+
f"{_SKILLS_LINE}",
|
|
135
|
+
file=sys.stderr,
|
|
136
|
+
)
|
|
@@ -1,23 +1,37 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import os
|
|
3
3
|
import tarfile
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
from pathlib import PurePath
|
|
5
6
|
|
|
6
7
|
logger = logging.getLogger("hivekit")
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ArchiveEntry:
|
|
12
|
+
"""One physical file and the relative name it should have in an archive."""
|
|
13
|
+
|
|
14
|
+
source_path: str
|
|
15
|
+
archive_name: str
|
|
16
|
+
|
|
17
|
+
|
|
9
18
|
def _is_glob(pattern: str) -> bool:
|
|
10
19
|
return any(c in pattern for c in "*?[")
|
|
11
20
|
|
|
12
21
|
|
|
22
|
+
def _validate_relative_path(rel_path: str) -> None:
|
|
23
|
+
"""Ensure a path is a safe relative archive name."""
|
|
24
|
+
if os.path.isabs(rel_path) or ".." in rel_path.split(os.sep):
|
|
25
|
+
raise ValueError(f"Unsafe relative path: '{rel_path}'")
|
|
26
|
+
|
|
27
|
+
|
|
13
28
|
def _validate_path(base_dir: str, rel_path: str) -> None:
|
|
14
29
|
"""Ensure rel_path resolves within base_dir and is a safe archive name."""
|
|
30
|
+
_validate_relative_path(rel_path)
|
|
15
31
|
real_base = os.path.realpath(base_dir)
|
|
16
32
|
real_full = os.path.realpath(os.path.join(base_dir, rel_path))
|
|
17
33
|
if not real_full.startswith(real_base + os.sep) and real_full != real_base:
|
|
18
34
|
raise ValueError(f"Path escapes base directory: '{rel_path}'")
|
|
19
|
-
if os.path.isabs(rel_path) or ".." in rel_path.split(os.sep):
|
|
20
|
-
raise ValueError(f"Unsafe relative path: '{rel_path}'")
|
|
21
35
|
|
|
22
36
|
|
|
23
37
|
def _walk_non_hidden(base_dir: str):
|
|
@@ -125,6 +139,25 @@ def add_to_tar(tar: tarfile.TarFile, base_dir: str, files: list[str]) -> None:
|
|
|
125
139
|
raise ValueError(f"Path is neither file nor directory: '{entry}'")
|
|
126
140
|
|
|
127
141
|
|
|
142
|
+
def add_entries_to_tar(tar: tarfile.TarFile, entries: list[ArchiveEntry]) -> None:
|
|
143
|
+
"""Add explicitly mapped regular files to an open tar archive."""
|
|
144
|
+
seen: set[str] = set()
|
|
145
|
+
for entry in entries:
|
|
146
|
+
_validate_relative_path(entry.archive_name)
|
|
147
|
+
if entry.archive_name in {"", "."}:
|
|
148
|
+
raise ValueError(f"Unsafe archive name: '{entry.archive_name}'")
|
|
149
|
+
if entry.archive_name in seen:
|
|
150
|
+
raise ValueError(f"Duplicate archive name: '{entry.archive_name}'")
|
|
151
|
+
seen.add(entry.archive_name)
|
|
152
|
+
|
|
153
|
+
source_path = os.path.expanduser(entry.source_path)
|
|
154
|
+
if os.path.islink(source_path):
|
|
155
|
+
raise ValueError(f"Symlinks are not supported: '{source_path}'")
|
|
156
|
+
if not os.path.isfile(source_path):
|
|
157
|
+
raise ValueError(f"Archive source is not a file: '{source_path}'")
|
|
158
|
+
tar.add(source_path, arcname=entry.archive_name, recursive=False)
|
|
159
|
+
|
|
160
|
+
|
|
128
161
|
def _add_directory(tar: tarfile.TarFile, base_dir: str, rel_dir: str) -> None:
|
|
129
162
|
"""Recursively add a directory, rejecting symlinks at any level."""
|
|
130
163
|
full_dir = os.path.join(base_dir, rel_dir)
|