hivekit 0.7.0__tar.gz → 0.8.0__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.7.0 → hivekit-0.8.0}/PKG-INFO +1 -1
- {hivekit-0.7.0 → hivekit-0.8.0}/pyproject.toml +2 -2
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/config.py +225 -5
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/experiment.py +16 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/http_client.py +25 -2
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/main.py +136 -9
- hivekit-0.8.0/src/cli/skills/hive-setup/SKILL.md +400 -0
- hivekit-0.8.0/src/cli/skills/hive-setup/references/configuration.md +200 -0
- hivekit-0.8.0/src/cli/skills/hive-setup/references/gpu-hardware.md +61 -0
- hivekit-0.8.0/src/cli/skills/hive-setup/references/multi-evaluator.md +119 -0
- hivekit-0.8.0/src/cli/skills_install.py +127 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/utils/config_paths.py +8 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/utils/url_utils.py +18 -1
- {hivekit-0.7.0 → hivekit-0.8.0}/src/hivekit.egg-info/PKG-INFO +1 -1
- {hivekit-0.7.0 → hivekit-0.8.0}/src/hivekit.egg-info/SOURCES.txt +5 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/tests/test_config.py +323 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/tests/test_experiment.py +46 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/tests/test_http_client.py +67 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/tests/test_main.py +192 -5
- {hivekit-0.7.0 → hivekit-0.8.0}/LICENSE +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/README.md +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/setup.cfg +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/__init__.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/auth/__init__.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/auth/auth_utils.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/auth/credential_store.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/auth/login_page.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/auth/logo.svg +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/auth/oidc_flow.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/auth/session_manager.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/auth/token_revoker.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/completers.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/utils/__init__.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/utils/archive.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/utils/config_sync.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/utils/docker.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/utils/logger.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/utils/terminal.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/utils/time.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/utils/upload.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/cli/version.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/hivekit.egg-info/dependency_links.txt +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/hivekit.egg-info/entry_points.txt +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/hivekit.egg-info/requires.txt +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/src/hivekit.egg-info/top_level.txt +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/tests/test_completers.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/tests/test_config_sync.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/tests/test_login.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/tests/test_logout.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/tests/test_overrides.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/tests/test_push_image.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/tests/test_time_utils.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/tests/test_upload.py +0 -0
- {hivekit-0.7.0 → hivekit-0.8.0}/tests/test_version.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hivekit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.8.0"
|
|
8
8
|
description = "Toolkits for using hive agents, including CLI and SDK."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -63,7 +63,7 @@ quote-style = "double"
|
|
|
63
63
|
include-package-data = true
|
|
64
64
|
|
|
65
65
|
[tool.setuptools.package-data]
|
|
66
|
-
"cli" = ["libs/**/*", "auth/*.svg"]
|
|
66
|
+
"cli" = ["libs/**/*", "auth/*.svg", "skills/**/*"]
|
|
67
67
|
|
|
68
68
|
[tool.pytest.ini_options]
|
|
69
69
|
pythonpath = ["src"]
|
|
@@ -19,6 +19,7 @@ from pydantic import (
|
|
|
19
19
|
model_validator,
|
|
20
20
|
)
|
|
21
21
|
|
|
22
|
+
from cli.utils.config_paths import get_coordinator_overlay_path
|
|
22
23
|
from cli.utils.logger import set_log_level
|
|
23
24
|
from cli.utils.time import unique_suffix
|
|
24
25
|
|
|
@@ -66,6 +67,29 @@ KNOWN_ACCELERATOR_TYPES = frozenset(
|
|
|
66
67
|
# the CLI before they reach the backend.
|
|
67
68
|
SUPPORTED_ARCHITECTURES = frozenset({"amd64", "arm64"})
|
|
68
69
|
|
|
70
|
+
# Allowed agent presets and known models for users to select from in the CLI
|
|
71
|
+
# when choosing custom agent ensemble configurations.
|
|
72
|
+
AGENT_PRESETS = frozenset({"low-effort", "medium-effort", "high-effort"})
|
|
73
|
+
KNOWN_MODELS = frozenset(
|
|
74
|
+
{
|
|
75
|
+
"anthropic.claude-haiku-4-5",
|
|
76
|
+
"anthropic.claude-opus-4-6",
|
|
77
|
+
"anthropic.claude-opus-4-7",
|
|
78
|
+
"anthropic.claude-opus-4-8",
|
|
79
|
+
"anthropic.claude-opus-5",
|
|
80
|
+
"anthropic.claude-sonnet-4-6",
|
|
81
|
+
"anthropic.claude-sonnet-5",
|
|
82
|
+
"moonshotai.kimi-k2.5",
|
|
83
|
+
"nvidia.nemotron-super-3-120b",
|
|
84
|
+
"openai.gpt-6-astra",
|
|
85
|
+
"openai.gpt-5.6-luna",
|
|
86
|
+
"openai.gpt-5.6-sol",
|
|
87
|
+
"openai.gpt-5.6-terra",
|
|
88
|
+
"qwen.qwen3-coder-480b-a35b",
|
|
89
|
+
"zai.glm-4.7-flash",
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
|
|
69
93
|
|
|
70
94
|
class ResourceConfig(BaseModel):
|
|
71
95
|
cpu: str = Field(
|
|
@@ -435,6 +459,71 @@ class ProviderConfig(BaseModel):
|
|
|
435
459
|
)
|
|
436
460
|
|
|
437
461
|
|
|
462
|
+
class AgentModelConfig(BaseModel):
|
|
463
|
+
model: str = Field(
|
|
464
|
+
description="The model this agent runs, named from the registry the Hive "
|
|
465
|
+
"ships. Run `hive list models` to see the names.",
|
|
466
|
+
)
|
|
467
|
+
weight: Optional[float] = Field(
|
|
468
|
+
default=None,
|
|
469
|
+
allow_inf_nan=False,
|
|
470
|
+
description="How heavily this model is drawn on relative to the rest of "
|
|
471
|
+
"the ensemble. Unset (the default) counts as 1.",
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
@field_validator("model")
|
|
475
|
+
@classmethod
|
|
476
|
+
def validate_model(cls, v):
|
|
477
|
+
if v not in KNOWN_MODELS:
|
|
478
|
+
raise ValueError(
|
|
479
|
+
f"Unknown model '{v}'. Known models: {', '.join(sorted(KNOWN_MODELS))}. "
|
|
480
|
+
"Run `hive list models` to see them."
|
|
481
|
+
)
|
|
482
|
+
return v
|
|
483
|
+
|
|
484
|
+
@field_validator("weight")
|
|
485
|
+
@classmethod
|
|
486
|
+
def validate_weight(cls, v):
|
|
487
|
+
if v is not None and v <= 0:
|
|
488
|
+
raise ValueError(f"weight must be greater than 0, got {v}.")
|
|
489
|
+
return v
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
class AgentsConfig(BaseModel):
|
|
493
|
+
preset: Optional[str] = Field(
|
|
494
|
+
default=None,
|
|
495
|
+
description="A preset agent configuration to run, by name.",
|
|
496
|
+
)
|
|
497
|
+
models: Optional[list[AgentModelConfig]] = Field(
|
|
498
|
+
default=None,
|
|
499
|
+
description="A custom ensemble of agent models to use in the Hive experiment.",
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
@field_validator("preset")
|
|
503
|
+
@classmethod
|
|
504
|
+
def validate_preset(cls, v):
|
|
505
|
+
if v is not None and v not in AGENT_PRESETS:
|
|
506
|
+
raise ValueError(
|
|
507
|
+
f"Unknown preset '{v}'. Valid presets: {', '.join(sorted(AGENT_PRESETS))}."
|
|
508
|
+
)
|
|
509
|
+
return v
|
|
510
|
+
|
|
511
|
+
@model_validator(mode="after")
|
|
512
|
+
def validate_exactly_one_way_of_choosing(self):
|
|
513
|
+
"""Fail closed unless exactly one of the two ways is used."""
|
|
514
|
+
if self.preset and self.models:
|
|
515
|
+
raise ValueError(
|
|
516
|
+
"agents names a preset tier or lists models, not both: "
|
|
517
|
+
"a preset already says which models to run."
|
|
518
|
+
)
|
|
519
|
+
if not self.preset and not self.models:
|
|
520
|
+
raise ValueError(
|
|
521
|
+
"agents must name a preset or list models. Omit the agents "
|
|
522
|
+
"section entirely to run the default tier."
|
|
523
|
+
)
|
|
524
|
+
return self
|
|
525
|
+
|
|
526
|
+
|
|
438
527
|
class RuntimeConfig(BaseModel):
|
|
439
528
|
num_sandboxes: int = Field(
|
|
440
529
|
default=1,
|
|
@@ -541,7 +630,10 @@ class HiveConfig(BaseModel):
|
|
|
541
630
|
coordinator_image_tag: Optional[str] = Field(
|
|
542
631
|
default=None,
|
|
543
632
|
description="The image tag to run the coordinator at, e.g. 'v0.6.0'. Optional — when "
|
|
544
|
-
"omitted,
|
|
633
|
+
"omitted, in a dev environment the tag recorded by `hivectl generate config` in "
|
|
634
|
+
"~/.hive/coordinator.yaml is used if present. If that file or tag is absent, or in a "
|
|
635
|
+
"production environment, the backend's default coordinator image is used. This field is "
|
|
636
|
+
"ignored in production environments.",
|
|
545
637
|
)
|
|
546
638
|
runtime: RuntimeConfig = Field(
|
|
547
639
|
default_factory=RuntimeConfig, description="Runtime configuration for the experiment."
|
|
@@ -561,6 +653,12 @@ class HiveConfig(BaseModel):
|
|
|
561
653
|
"omitted, the default provider is used with no capacity-type constraint.",
|
|
562
654
|
)
|
|
563
655
|
prompt: Optional[PromptConfig] = None
|
|
656
|
+
agents: Optional[AgentsConfig] = Field(
|
|
657
|
+
default=None,
|
|
658
|
+
description="Which agents generate new versions of the code: a preset "
|
|
659
|
+
"configuration, or a custom ensemble of models you pick. Optional — "
|
|
660
|
+
"when omitted, the Hive runs its own default tier.",
|
|
661
|
+
)
|
|
564
662
|
log_level: str = Field(
|
|
565
663
|
default="WARNING",
|
|
566
664
|
json_schema_extra={"enumerated": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]},
|
|
@@ -579,6 +677,49 @@ class HiveConfig(BaseModel):
|
|
|
579
677
|
def raw_yaml(self, value: Optional[str]) -> None:
|
|
580
678
|
self._raw_yaml = value
|
|
581
679
|
|
|
680
|
+
@model_validator(mode="before")
|
|
681
|
+
@classmethod
|
|
682
|
+
def migrate_coordinator_config_name(cls, values):
|
|
683
|
+
# The effort-tier coordinator configs are the deprecated spelling of the
|
|
684
|
+
# matching agents.preset. Accept them, warn, and fold them into the
|
|
685
|
+
# canonical field. Setting agents too is a fatal error -- the legacy
|
|
686
|
+
# field would be redundant and we refuse to guess which one wins. Names
|
|
687
|
+
# outside that set have no agents equivalent, so they pass through under
|
|
688
|
+
# a plain warning.
|
|
689
|
+
if not isinstance(values, dict) or "coordinator_config_name" not in values:
|
|
690
|
+
return values
|
|
691
|
+
# Copy first: model_validate passes the caller's own dict through, and we
|
|
692
|
+
# must not mutate it -- not on success, and not on the conflict-raise path.
|
|
693
|
+
values = dict(values)
|
|
694
|
+
legacy = values.pop("coordinator_config_name")
|
|
695
|
+
match legacy:
|
|
696
|
+
case "high-effort-config" | "medium-effort-config" | "low-effort-config":
|
|
697
|
+
preset = legacy.removesuffix("-config")
|
|
698
|
+
if values.get("agents") is not None:
|
|
699
|
+
raise ValueError(
|
|
700
|
+
f"coordinator_config_name={legacy} and agents are both set. "
|
|
701
|
+
"coordinator_config_name is deprecated and now just selects "
|
|
702
|
+
f"agents.preset={preset}; set only agents."
|
|
703
|
+
)
|
|
704
|
+
# agents is absent (conflicts raised above), so this never
|
|
705
|
+
# discards an explicit value.
|
|
706
|
+
values["agents"] = {"preset": preset}
|
|
707
|
+
logger.warning(
|
|
708
|
+
"coordinator_config_name=%s is deprecated. Running agents.preset=%s "
|
|
709
|
+
"instead; set agents.preset (or agents.models) in your config to "
|
|
710
|
+
"silence this warning.",
|
|
711
|
+
legacy,
|
|
712
|
+
preset,
|
|
713
|
+
)
|
|
714
|
+
case _:
|
|
715
|
+
values["coordinator_config_name"] = legacy
|
|
716
|
+
logger.warning(
|
|
717
|
+
"coordinator_config_name=%s is deprecated. Specify the agent/models "
|
|
718
|
+
"used under the agents.preset or agents.models fields instead.",
|
|
719
|
+
legacy,
|
|
720
|
+
)
|
|
721
|
+
return values
|
|
722
|
+
|
|
582
723
|
|
|
583
724
|
def _lookup_description(schema: dict, loc: tuple) -> str | None:
|
|
584
725
|
"""Walk the JSON schema to find the description for a field at the given location."""
|
|
@@ -654,7 +795,7 @@ def validate_repo_files_exist(base_dir: str, repo: RepoConfig, allow_missing: bo
|
|
|
654
795
|
)
|
|
655
796
|
|
|
656
797
|
|
|
657
|
-
def apply_overrides(cfg: dict, overrides: list[str]) ->
|
|
798
|
+
def apply_overrides(cfg: dict, overrides: list[str]) -> set[tuple[str, ...]]:
|
|
658
799
|
"""Apply dot-notation overrides to a raw config dict before Pydantic validation.
|
|
659
800
|
|
|
660
801
|
Supports two forms:
|
|
@@ -664,6 +805,9 @@ def apply_overrides(cfg: dict, overrides: list[str]) -> dict:
|
|
|
664
805
|
Values are coerced via yaml.safe_load, so 4 -> int, true -> bool, etc.
|
|
665
806
|
Type correctness and field existence within typed models are enforced by
|
|
666
807
|
Pydantic at construction time.
|
|
808
|
+
|
|
809
|
+
``cfg`` is edited in place; the key paths the overrides set are returned, so a
|
|
810
|
+
caller can tell a value the command line asked for from one the file carried.
|
|
667
811
|
"""
|
|
668
812
|
overridden_keys: set[tuple[str, ...]] = set()
|
|
669
813
|
for raw in overrides:
|
|
@@ -682,7 +826,7 @@ def apply_overrides(cfg: dict, overrides: list[str]) -> dict:
|
|
|
682
826
|
d[parts[-1]] = value
|
|
683
827
|
overridden_keys.add(tuple(parts))
|
|
684
828
|
_reconcile_num_sandboxes_override(cfg, overridden_keys)
|
|
685
|
-
return
|
|
829
|
+
return overridden_keys
|
|
686
830
|
|
|
687
831
|
|
|
688
832
|
def _reconcile_num_sandboxes_override(cfg: dict, overridden_keys: set[tuple[str, ...]]) -> None:
|
|
@@ -718,17 +862,93 @@ def _reconcile_num_sandboxes_override(cfg: dict, overridden_keys: set[tuple[str,
|
|
|
718
862
|
del runtime[loser]
|
|
719
863
|
|
|
720
864
|
|
|
865
|
+
def _recorded_coordinator_image_tag(overlay_path: str) -> Optional[str]:
|
|
866
|
+
"""Return the coordinator image tag recorded in the overlay file, if any.
|
|
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"]
|
|
914
|
+
source = "a command-line override" if overridden else "the experiment config"
|
|
915
|
+
# str(): an unquoted digit-only tag loads as an int. None is not compared, or
|
|
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 != "":
|
|
927
|
+
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
|
+
|
|
931
|
+
|
|
721
932
|
def load_config(
|
|
722
933
|
file_path: str,
|
|
723
934
|
overrides: list[str] | None = None,
|
|
935
|
+
coordinator: bool = True,
|
|
724
936
|
) -> HiveConfig:
|
|
725
|
-
"""Load configuration from a YAML file.
|
|
937
|
+
"""Load configuration from a YAML file.
|
|
938
|
+
|
|
939
|
+
Pass ``coordinator=False`` when no coordinator will run, so the recorded
|
|
940
|
+
coordinator image tag is neither filled in nor reported as being used.
|
|
941
|
+
"""
|
|
726
942
|
with open(file_path, "r") as file:
|
|
727
943
|
raw_text = file.read()
|
|
728
944
|
config_data = yaml.safe_load(raw_text)
|
|
729
945
|
|
|
946
|
+
overridden_keys: set[tuple[str, ...]] = set()
|
|
730
947
|
if overrides:
|
|
731
|
-
apply_overrides(config_data, overrides)
|
|
948
|
+
overridden_keys = apply_overrides(config_data, overrides)
|
|
949
|
+
|
|
950
|
+
if coordinator and isinstance(config_data, dict):
|
|
951
|
+
_resolve_coordinator_image_tag(config_data, ("coordinator_image_tag",) in overridden_keys)
|
|
732
952
|
|
|
733
953
|
try:
|
|
734
954
|
config = HiveConfig(**config_data)
|
|
@@ -129,6 +129,22 @@ def build_experiment_crd(config: HiveConfig) -> Dict[str, Any]:
|
|
|
129
129
|
if config.prompt.enable_evolution:
|
|
130
130
|
experiment["spec"]["prompt"]["enableEvolution"] = config.prompt.enable_evolution
|
|
131
131
|
|
|
132
|
+
# Add optional agent selection
|
|
133
|
+
if config.agents:
|
|
134
|
+
agents = {}
|
|
135
|
+
if config.agents.preset:
|
|
136
|
+
agents["preset"] = config.agents.preset
|
|
137
|
+
if config.agents.models:
|
|
138
|
+
agents["models"] = [
|
|
139
|
+
{
|
|
140
|
+
"model": entry.model,
|
|
141
|
+
**({"weight": entry.weight} if entry.weight is not None else {}),
|
|
142
|
+
}
|
|
143
|
+
for entry in config.agents.models
|
|
144
|
+
]
|
|
145
|
+
if agents:
|
|
146
|
+
experiment["spec"]["agents"] = agents
|
|
147
|
+
|
|
132
148
|
# Add coordinator config
|
|
133
149
|
if config.coordinator_config_name:
|
|
134
150
|
experiment["spec"]["coordinatorConfigName"] = config.coordinator_config_name
|
|
@@ -369,16 +369,39 @@ class HttpClient:
|
|
|
369
369
|
response = self._send("get", "/experiments", "list experiments")
|
|
370
370
|
return response.json()
|
|
371
371
|
|
|
372
|
+
def start_repo_download(self, name: str, content_uid: str) -> Dict[str, Any]:
|
|
373
|
+
"""
|
|
374
|
+
Initiate assembly of the repository at a content snapshot.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
name: Experiment name.
|
|
378
|
+
content_uid: Content snapshot identifier (a point on the evolution chart).
|
|
379
|
+
|
|
380
|
+
Returns:
|
|
381
|
+
Object with ``status`` ("ready" when already assembled, "started" when
|
|
382
|
+
assembly was kicked off). ``_send`` treats both 200 and 202 as success.
|
|
383
|
+
"""
|
|
384
|
+
response = self._send(
|
|
385
|
+
"post",
|
|
386
|
+
f"/experiments/{name}/repo-download",
|
|
387
|
+
"start repo download",
|
|
388
|
+
params={"contentUid": content_uid},
|
|
389
|
+
)
|
|
390
|
+
return response.json()
|
|
391
|
+
|
|
372
392
|
def get_repo_download_url(self, name: str, content_uid: str) -> Dict[str, Any]:
|
|
373
393
|
"""
|
|
374
|
-
|
|
394
|
+
Poll for a presigned URL to download the repository assembled at a content snapshot.
|
|
375
395
|
|
|
376
396
|
Args:
|
|
377
397
|
name: Experiment name.
|
|
378
398
|
content_uid: Content snapshot identifier (a point on the evolution chart).
|
|
379
399
|
|
|
380
400
|
Returns:
|
|
381
|
-
Object with ``url`` (presigned S3 URL) and ``expiresIn`` (seconds)
|
|
401
|
+
Object with ``url`` (presigned S3 URL) and ``expiresIn`` (seconds) when
|
|
402
|
+
ready, or ``{"status": "pending"}`` while assembly is still in progress
|
|
403
|
+
(the server answers a not-yet-ready poll with 202, which ``_send`` does
|
|
404
|
+
not treat as an error).
|
|
382
405
|
"""
|
|
383
406
|
response = self._send(
|
|
384
407
|
"get",
|
|
@@ -29,13 +29,15 @@ from cli.auth.credential_store import create_credential_store
|
|
|
29
29
|
from cli.auth.session_manager import create_session_manager
|
|
30
30
|
from cli.auth.token_revoker import TokenRevocationError, TokenRevoker
|
|
31
31
|
from cli.completers import config_file_completer, experiment_completer
|
|
32
|
-
from cli.config import load_config, validate_repo_files_exist
|
|
32
|
+
from cli.config import KNOWN_MODELS, load_config, validate_repo_files_exist
|
|
33
33
|
from cli.http_client import (
|
|
34
34
|
HttpClient,
|
|
35
35
|
ShellNotFoundError,
|
|
36
36
|
StreamNotReadyError,
|
|
37
37
|
create_http_client,
|
|
38
38
|
)
|
|
39
|
+
from cli.skills_install import TARGETS as SKILL_TARGETS
|
|
40
|
+
from cli.skills_install import skills_install
|
|
39
41
|
from cli.utils import docker
|
|
40
42
|
from cli.utils.archive import resolve_file_list
|
|
41
43
|
from cli.utils.config_paths import get_config_dir, get_config_path, load_organization_id
|
|
@@ -47,6 +49,7 @@ from cli.utils.url_utils import (
|
|
|
47
49
|
derive_identity_base_url,
|
|
48
50
|
get_api_endpoint,
|
|
49
51
|
get_dashboard_url,
|
|
52
|
+
get_experiment_url,
|
|
50
53
|
)
|
|
51
54
|
from cli.version import print_version
|
|
52
55
|
|
|
@@ -327,7 +330,7 @@ def create_experiment(args, overrides) -> None:
|
|
|
327
330
|
experiment_crd["metadata"]["annotations"]["hiverge.ai/config-tmp-id"] = config_tmp_id
|
|
328
331
|
|
|
329
332
|
try:
|
|
330
|
-
client.create_experiment(experiment_crd)
|
|
333
|
+
created = client.create_experiment(experiment_crd)
|
|
331
334
|
logger.info("Created experiment. experiment_name=%s", config.experiment_name)
|
|
332
335
|
console.print(f'experiment "[cyan]{config.experiment_name}[/cyan]" created')
|
|
333
336
|
except Exception as e:
|
|
@@ -335,6 +338,30 @@ def create_experiment(args, overrides) -> None:
|
|
|
335
338
|
console.print(f"[bold red]Error:[/bold red] {e}", highlight=False)
|
|
336
339
|
sys.exit(1)
|
|
337
340
|
|
|
341
|
+
url = _experiment_url(created)
|
|
342
|
+
if url:
|
|
343
|
+
console.print(f" • [bold]Dashboard:[/bold] {url}", highlight=False, soft_wrap=True)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _experiment_url(created: dict[str, object]) -> str | None:
|
|
347
|
+
"""
|
|
348
|
+
Dashboard URL for a just-created experiment, or None if it cannot be built.
|
|
349
|
+
|
|
350
|
+
``created`` is the CRD the server echoes back; the Alphatrion experiment ID
|
|
351
|
+
the dashboard routes on is its ``hiverge.ai/experiment-id`` label.
|
|
352
|
+
"""
|
|
353
|
+
try:
|
|
354
|
+
labels = created["metadata"]["labels"] # type: ignore[index]
|
|
355
|
+
experiment_id = labels["hiverge.ai/experiment-id"]
|
|
356
|
+
if not experiment_id:
|
|
357
|
+
return None
|
|
358
|
+
return get_experiment_url(
|
|
359
|
+
experiment_id=experiment_id, organization_id=load_organization_id()
|
|
360
|
+
)
|
|
361
|
+
except Exception:
|
|
362
|
+
logger.debug("Could not build the dashboard URL for the new experiment", exc_info=True)
|
|
363
|
+
return None
|
|
364
|
+
|
|
338
365
|
|
|
339
366
|
def shell_command(args, overrides) -> None:
|
|
340
367
|
"""Create an interactive shell and attach to it.
|
|
@@ -408,7 +435,8 @@ def shell_command(args, overrides) -> None:
|
|
|
408
435
|
# A shell has no experiment; force a throwaway name to satisfy validation.
|
|
409
436
|
overrides = [*overrides, "experiment_name=shell"]
|
|
410
437
|
try:
|
|
411
|
-
|
|
438
|
+
# A shell runs no coordinator, so its image tag does not apply.
|
|
439
|
+
config = load_config(args.config, overrides=overrides, coordinator=False)
|
|
412
440
|
except Exception as e:
|
|
413
441
|
console.print(
|
|
414
442
|
f"[bold red]Error:[/bold red] invalid config '{args.config}'.\n{e}", highlight=False
|
|
@@ -930,6 +958,14 @@ def list_coordinators(args) -> None:
|
|
|
930
958
|
return
|
|
931
959
|
|
|
932
960
|
|
|
961
|
+
def list_models(args) -> None:
|
|
962
|
+
"""List the models an agent ensemble can name in a config's `agents.models`."""
|
|
963
|
+
console = Console()
|
|
964
|
+
|
|
965
|
+
for model in sorted(KNOWN_MODELS):
|
|
966
|
+
console.print(model, highlight=False)
|
|
967
|
+
|
|
968
|
+
|
|
933
969
|
def get_experiment(args) -> None:
|
|
934
970
|
"""Get details of a specific experiment."""
|
|
935
971
|
console = Console()
|
|
@@ -1015,6 +1051,19 @@ def get_experiment(args) -> None:
|
|
|
1015
1051
|
console.print(f" name: {provider.get('name')}", highlight=False)
|
|
1016
1052
|
if provider.get("spot") is not None:
|
|
1017
1053
|
console.print(f" spot: {provider.get('spot')}")
|
|
1054
|
+
# Agents subsection. Only shown when the experiment selected agents.
|
|
1055
|
+
agents = spec.get("agents", {})
|
|
1056
|
+
if agents:
|
|
1057
|
+
console.print(" [bold]agents[/bold]")
|
|
1058
|
+
if agents.get("preset"):
|
|
1059
|
+
console.print(f" preset: {agents.get('preset')}", highlight=False)
|
|
1060
|
+
for entry in agents.get("models", []):
|
|
1061
|
+
# One line per model, weight only when the experiment asked for
|
|
1062
|
+
# it, matching what was submitted.
|
|
1063
|
+
details = [f"model: {entry.get('model', 'N/A')}"]
|
|
1064
|
+
if entry.get("weight") is not None:
|
|
1065
|
+
details.append(f"weight: {entry['weight']}")
|
|
1066
|
+
console.print(f" - {', '.join(details)}", highlight=False)
|
|
1018
1067
|
# Status section
|
|
1019
1068
|
created = metadata.get("creationTimestamp", "N/A")
|
|
1020
1069
|
age = humanize_time(created)
|
|
@@ -1065,6 +1114,32 @@ def get_config(args) -> None:
|
|
|
1065
1114
|
console.print(hive_yaml.rstrip("\n"), highlight=False, markup=False)
|
|
1066
1115
|
|
|
1067
1116
|
|
|
1117
|
+
def _poll_repo_download_url(
|
|
1118
|
+
client: HttpClient, name: str, content_uid: str, console: Console
|
|
1119
|
+
) -> str:
|
|
1120
|
+
"""Poll until the assembled repository is ready, then return its presigned URL.
|
|
1121
|
+
|
|
1122
|
+
Exits the process on timeout. A permanent server failure propagates as an
|
|
1123
|
+
exception raised by ``get_repo_download_url`` (handled by the caller).
|
|
1124
|
+
"""
|
|
1125
|
+
timeout = 3600
|
|
1126
|
+
backoff = 1.0
|
|
1127
|
+
backoff_cap = 5.0
|
|
1128
|
+
deadline = time.monotonic() + timeout
|
|
1129
|
+
|
|
1130
|
+
with console.status("Assembling repository..."):
|
|
1131
|
+
while time.monotonic() < deadline:
|
|
1132
|
+
result = client.get_repo_download_url(name, content_uid)
|
|
1133
|
+
url = result.get("url")
|
|
1134
|
+
if url:
|
|
1135
|
+
return url
|
|
1136
|
+
time.sleep(backoff)
|
|
1137
|
+
backoff = min(backoff * 2, backoff_cap)
|
|
1138
|
+
|
|
1139
|
+
console.print("[red]Error: timed out waiting for repository assembly[/red]")
|
|
1140
|
+
sys.exit(1)
|
|
1141
|
+
|
|
1142
|
+
|
|
1068
1143
|
def download_repo(args) -> None:
|
|
1069
1144
|
"""Download the repository assembled at a content snapshot of an experiment."""
|
|
1070
1145
|
console = Console()
|
|
@@ -1080,21 +1155,46 @@ def download_repo(args) -> None:
|
|
|
1080
1155
|
|
|
1081
1156
|
try:
|
|
1082
1157
|
client = _get_http_client(args)
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1158
|
+
|
|
1159
|
+
# Kick off assembly. A permanent failure (bad args, unauthorized, not
|
|
1160
|
+
# found, internal) raises here and is caught below.
|
|
1161
|
+
client.start_repo_download(experiment_name, content_uid)
|
|
1162
|
+
|
|
1163
|
+
url = _poll_repo_download_url(client, experiment_name, content_uid, console)
|
|
1164
|
+
|
|
1165
|
+
from rich.progress import (
|
|
1166
|
+
BarColumn,
|
|
1167
|
+
DownloadColumn,
|
|
1168
|
+
Progress,
|
|
1169
|
+
TextColumn,
|
|
1170
|
+
TimeRemainingColumn,
|
|
1171
|
+
TransferSpeedColumn,
|
|
1172
|
+
)
|
|
1088
1173
|
|
|
1089
1174
|
# The presigned URL is a plain S3 GET; do not send Hive auth headers to it.
|
|
1090
1175
|
with requests.get(
|
|
1091
1176
|
url, stream=True, timeout=300, verify=not getattr(args, "insecure", False)
|
|
1092
1177
|
) as resp:
|
|
1093
1178
|
resp.raise_for_status()
|
|
1094
|
-
|
|
1179
|
+
# S3 returns Content-Length for the object, so the bar is determinate;
|
|
1180
|
+
# if it were ever absent, total=None degrades to a bytes counter.
|
|
1181
|
+
total = int(resp.headers.get("Content-Length") or 0) or None
|
|
1182
|
+
with (
|
|
1183
|
+
Progress(
|
|
1184
|
+
TextColumn("[progress.description]{task.description}"),
|
|
1185
|
+
BarColumn(),
|
|
1186
|
+
DownloadColumn(),
|
|
1187
|
+
TransferSpeedColumn(),
|
|
1188
|
+
TimeRemainingColumn(),
|
|
1189
|
+
console=console,
|
|
1190
|
+
) as progress,
|
|
1191
|
+
open(output, "wb") as f,
|
|
1192
|
+
):
|
|
1193
|
+
task = progress.add_task("Downloading", total=total)
|
|
1095
1194
|
for chunk in resp.iter_content(chunk_size=1 << 16):
|
|
1096
1195
|
if chunk: # skip empty keep-alive chunks
|
|
1097
1196
|
f.write(chunk)
|
|
1197
|
+
progress.update(task, advance=len(chunk))
|
|
1098
1198
|
except Exception as e:
|
|
1099
1199
|
console.print(f"[red]Error: Failed to download repo: {e}[/red]")
|
|
1100
1200
|
sys.exit(1)
|
|
@@ -1407,6 +1507,14 @@ def main():
|
|
|
1407
1507
|
)
|
|
1408
1508
|
parser_list_image.set_defaults(func=list_images)
|
|
1409
1509
|
|
|
1510
|
+
# list models
|
|
1511
|
+
parser_list_models = list_subparsers.add_parser(
|
|
1512
|
+
"models",
|
|
1513
|
+
aliases=["model"],
|
|
1514
|
+
help="List the models an agent ensemble can name",
|
|
1515
|
+
)
|
|
1516
|
+
parser_list_models.set_defaults(func=list_models)
|
|
1517
|
+
|
|
1410
1518
|
# delete command
|
|
1411
1519
|
parser_delete = subparsers.add_parser("delete", help="Delete resources")
|
|
1412
1520
|
parser_delete.set_defaults(func=lambda _: parser_delete.print_help())
|
|
@@ -1497,6 +1605,25 @@ def main():
|
|
|
1497
1605
|
)
|
|
1498
1606
|
parser_logs.set_defaults(func=stream_logs)
|
|
1499
1607
|
|
|
1608
|
+
parser_skills = subparsers.add_parser("skills", help="Manage bundled coding-agent skills")
|
|
1609
|
+
parser_skills.set_defaults(func=lambda _: parser_skills.print_help())
|
|
1610
|
+
skills_subparsers = parser_skills.add_subparsers(dest="skills_target")
|
|
1611
|
+
parser_skills_install = skills_subparsers.add_parser(
|
|
1612
|
+
"install",
|
|
1613
|
+
help="Install bundled skills into a coding agent's skills directory",
|
|
1614
|
+
)
|
|
1615
|
+
parser_skills_install.add_argument(
|
|
1616
|
+
"who",
|
|
1617
|
+
nargs="*",
|
|
1618
|
+
choices=list(SKILL_TARGETS),
|
|
1619
|
+
metavar="AGENT",
|
|
1620
|
+
help="agents to install for: " + ", ".join(SKILL_TARGETS),
|
|
1621
|
+
)
|
|
1622
|
+
parser_skills_install.add_argument(
|
|
1623
|
+
"--all", action="store_true", help="install for every supported agent"
|
|
1624
|
+
)
|
|
1625
|
+
parser_skills_install.set_defaults(func=skills_install)
|
|
1626
|
+
|
|
1500
1627
|
argcomplete.autocomplete(parser)
|
|
1501
1628
|
args, overrides = parser.parse_known_args()
|
|
1502
1629
|
if hasattr(args, "func"):
|