techtree 0.1.0__py3-none-any.whl
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.
- techtree/__init__.py +35 -0
- techtree/__main__.py +14 -0
- techtree/canonical.py +239 -0
- techtree/catalog/__init__.py +25 -0
- techtree/catalog/repository.py +400 -0
- techtree/catalog/service.py +419 -0
- techtree/cli/__init__.py +1 -0
- techtree/cli/app.py +416 -0
- techtree/cli/commands/__init__.py +1 -0
- techtree/cli/commands/climb.py +1223 -0
- techtree/cli/commands/doctor.py +147 -0
- techtree/cli/commands/engine.py +207 -0
- techtree/cli/commands/proof.py +556 -0
- techtree/cli/commands/publish.py +447 -0
- techtree/cli/commands/release.py +303 -0
- techtree/cli/commands/run.py +1067 -0
- techtree/cli/commands/setup.py +181 -0
- techtree/cli/commands/skill.py +221 -0
- techtree/cli/commands/uplift.py +698 -0
- techtree/cli/commands/withdraw.py +212 -0
- techtree/cli/confirm.py +47 -0
- techtree/cli/context.py +96 -0
- techtree/cli/invoke.py +220 -0
- techtree/cli/output.py +280 -0
- techtree/constants.py +138 -0
- techtree/crypto.py +128 -0
- techtree/doctor/__init__.py +1 -0
- techtree/doctor/checks.py +675 -0
- techtree/doctor/execution_checks.py +435 -0
- techtree/doctor/service.py +326 -0
- techtree/drafts/__init__.py +32 -0
- techtree/drafts/source.py +146 -0
- techtree/drafts/store.py +992 -0
- techtree/engines/__init__.py +1 -0
- techtree/engines/bundle.py +251 -0
- techtree/engines/installer.py +679 -0
- techtree/engines/registry.py +235 -0
- techtree/engines/runner.py +170 -0
- techtree/errors.py +262 -0
- techtree/fs.py +234 -0
- techtree/harness.py +108 -0
- techtree/identity/__init__.py +41 -0
- techtree/identity/models.py +113 -0
- techtree/identity/service.py +199 -0
- techtree/identity/store.py +263 -0
- techtree/ids.py +85 -0
- techtree/manifests/__init__.py +39 -0
- techtree/manifests/builder.py +433 -0
- techtree/manifests/compare.py +376 -0
- techtree/models/__init__.py +282 -0
- techtree/models/base.py +201 -0
- techtree/models/campaign.py +484 -0
- techtree/models/catalog.py +227 -0
- techtree/models/cli.py +151 -0
- techtree/models/climb.py +254 -0
- techtree/models/data_policy.py +130 -0
- techtree/models/engine.py +156 -0
- techtree/models/episode_receipt.py +130 -0
- techtree/models/evaluation_backend.py +113 -0
- techtree/models/experiment.py +154 -0
- techtree/models/run.py +214 -0
- techtree/models/skill.py +156 -0
- techtree/models/uplift_report.py +158 -0
- techtree/models/validation.py +299 -0
- techtree/paths.py +116 -0
- techtree/presentation/__init__.py +31 -0
- techtree/presentation/build.py +1242 -0
- techtree/presentation/compact.py +246 -0
- techtree/presentation/evidence.py +169 -0
- techtree/presentation/models.py +358 -0
- techtree/presentation/rich.py +312 -0
- techtree/presentation/sanitize.py +156 -0
- techtree/publication/__init__.py +44 -0
- techtree/publication/address.py +180 -0
- techtree/publication/coordinates.py +26 -0
- techtree/publication/journal.py +212 -0
- techtree/publication/keccak.py +183 -0
- techtree/publication/models.py +209 -0
- techtree/publication/offer.py +35 -0
- techtree/publication/service.py +618 -0
- techtree/publication/transport.py +296 -0
- techtree/publication/verify.py +242 -0
- techtree/publication/withdraw.py +156 -0
- techtree/py.typed +0 -0
- techtree/receipts/__init__.py +52 -0
- techtree/receipts/bundle.py +578 -0
- techtree/receipts/compare.py +1065 -0
- techtree/receipts/episode.py +672 -0
- techtree/receipts/execution.py +630 -0
- techtree/receipts/observed.py +474 -0
- techtree/receipts/set.py +336 -0
- techtree/receipts/uplift.py +655 -0
- techtree/receipts/verify.py +1055 -0
- techtree/release/__init__.py +9 -0
- techtree/release/bootstrap.py +509 -0
- techtree/release/checks.py +376 -0
- techtree/release/document.py +125 -0
- techtree/release/generate.py +221 -0
- techtree/release/models.py +293 -0
- techtree/release/provenance.py +109 -0
- techtree/resources/catalog/campaigns/hello-world-climb.json +1 -0
- techtree/resources/catalog/catalog.json +32 -0
- techtree/resources/catalog/climbs/hello-world-climb.json +1 -0
- techtree/resources/catalog/data-policies/hello-world-climb.json +1 -0
- techtree/resources/catalog/taskset-validations/hello-world-climb.json +1 -0
- techtree/resources/catalog/validation-evidence/hello-world-climb.json +1 -0
- techtree/resources/engines/default/engine.json +20 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/__init__.py +7 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/algorithm.py +136 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/dataset.py +156 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/env.py +48 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/procedure_transfer_v1/taskset.py +163 -0
- techtree/resources/engines/default/packages/procedure-transfer-v1/pyproject.toml +13 -0
- techtree/resources/engines/default/pyproject.toml +23 -0
- techtree/resources/engines/default/tools/inspect_taskset.py +124 -0
- techtree/resources/engines/default/tools/normalize_eval_output.py +470 -0
- techtree/resources/engines/default/tools/normalize_validation.py +222 -0
- techtree/resources/engines/default/uv.lock +1758 -0
- techtree/resources/harness/hermes-agent-0.19.0.json +69 -0
- techtree/resources/release/build-provenance.json +4 -0
- techtree/resources/release/release-core.json +24 -0
- techtree/runs/__init__.py +31 -0
- techtree/runs/artifacts.py +750 -0
- techtree/runs/child_registry.py +228 -0
- techtree/runs/events.py +478 -0
- techtree/runs/executor.py +140 -0
- techtree/runs/fake.py +741 -0
- techtree/runs/launcher.py +253 -0
- techtree/runs/machine.py +489 -0
- techtree/runs/real.py +789 -0
- techtree/runs/service.py +616 -0
- techtree/runs/store.py +555 -0
- techtree/runs/validation.py +259 -0
- techtree/runs/variants.py +684 -0
- techtree/settings.py +143 -0
- techtree/skills/__init__.py +14 -0
- techtree/skills/archive.py +282 -0
- techtree/skills/policy.py +62 -0
- techtree/skills/scanner.py +394 -0
- techtree/skills/service.py +752 -0
- techtree/skills/starter.py +434 -0
- techtree/tasksets/__init__.py +1 -0
- techtree/tasksets/membership.py +269 -0
- techtree/tasksets/provider.py +207 -0
- techtree/tasksets/resolver.py +311 -0
- techtree/tasksets/service.py +484 -0
- techtree/tasksets/verifiers_cli.py +538 -0
- techtree/uplift/__init__.py +20 -0
- techtree/uplift/context.py +544 -0
- techtree/uplift/derive.py +203 -0
- techtree/uplift/public_tasks.py +151 -0
- techtree/uplift/service.py +719 -0
- techtree/uplift/source.py +160 -0
- techtree/verifiers/__init__.py +31 -0
- techtree/verifiers/budget.py +219 -0
- techtree/verifiers/child.py +633 -0
- techtree/verifiers/compiler.py +432 -0
- techtree/verifiers/config.py +365 -0
- techtree/verifiers/credentials.py +321 -0
- techtree/verifiers/image.py +126 -0
- techtree/verifiers/models.py +527 -0
- techtree/verifiers/outputs.py +368 -0
- techtree/verifiers/progress.py +192 -0
- techtree/verifiers/supervisor.py +341 -0
- techtree/verifiers/verify.py +782 -0
- techtree/version.py +39 -0
- techtree/worker/__init__.py +18 -0
- techtree/worker/execute.py +487 -0
- techtree/worker/main.py +57 -0
- techtree-0.1.0.dist-info/METADATA +344 -0
- techtree-0.1.0.dist-info/RECORD +174 -0
- techtree-0.1.0.dist-info/WHEEL +4 -0
- techtree-0.1.0.dist-info/entry_points.txt +3 -0
- techtree-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""One resolved experiment becomes one Verifiers configuration. Spec section 6.8.
|
|
2
|
+
|
|
3
|
+
Compilation is a translation with no judgement in it. Everything the compiler
|
|
4
|
+
could have decided has already been decided by the Campaign and frozen into the
|
|
5
|
+
two experiment manifests, and this module's job is to refuse rather than to
|
|
6
|
+
reconcile: if a manifest and its Campaign disagree about anything at all, the
|
|
7
|
+
right answer is a named error, not a compiled document that splits the
|
|
8
|
+
difference.
|
|
9
|
+
|
|
10
|
+
Determinism is the property the rest of WP6 leans on. The same manifest, the
|
|
11
|
+
same run paths and the same variant always produce byte-identical JSON, which
|
|
12
|
+
is what lets a resolved config be compared against a compiled one and what lets
|
|
13
|
+
a run be reproduced from its own inputs.
|
|
14
|
+
|
|
15
|
+
The evaluation client is derived rather than chosen. Techtree emits the
|
|
16
|
+
credential variable the Campaign declared and omits ``base_url`` entirely, so a
|
|
17
|
+
Campaign that declares ``PRIME_API_KEY`` gets the pinned client's Prime
|
|
18
|
+
resolution (spec section 6.8's Prime profile) without Techtree writing a
|
|
19
|
+
provider URL into a run's inputs. There is no provider table here; which
|
|
20
|
+
provider a name belongs to is ``credentials``' question, not the compiler's.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import math
|
|
26
|
+
import os
|
|
27
|
+
from collections.abc import Mapping
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Final, NoReturn
|
|
30
|
+
|
|
31
|
+
from techtree.canonical import digest_object, sha256_digest_bytes
|
|
32
|
+
from techtree.errors import ValidationError
|
|
33
|
+
from techtree.fs import ensure_private_directory, fsync_directory, open_exclusive
|
|
34
|
+
from techtree.models.base import ArtifactRef, Digest
|
|
35
|
+
from techtree.models.campaign import (
|
|
36
|
+
SUBJECT_AGENT,
|
|
37
|
+
AgentSpec,
|
|
38
|
+
CampaignSpec,
|
|
39
|
+
VariantSchedule,
|
|
40
|
+
)
|
|
41
|
+
from techtree.models.experiment import ExperimentManifest, ExperimentVariant
|
|
42
|
+
from techtree.verifiers.config import (
|
|
43
|
+
DockerRuntimeToml,
|
|
44
|
+
EnvToml,
|
|
45
|
+
EvalClientToml,
|
|
46
|
+
EvalToml,
|
|
47
|
+
HermesHarnessToml,
|
|
48
|
+
SamplingToml,
|
|
49
|
+
SubjectAgentToml,
|
|
50
|
+
TasksetToml,
|
|
51
|
+
TimeoutToml,
|
|
52
|
+
config_to_json_bytes,
|
|
53
|
+
egress_for,
|
|
54
|
+
)
|
|
55
|
+
from techtree.verifiers.models import (
|
|
56
|
+
EVAL_RUN_NAME,
|
|
57
|
+
RunPaths,
|
|
58
|
+
VariantExecutionPlan,
|
|
59
|
+
VariantName,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
__all__ = [
|
|
63
|
+
"EVAL_CONFIG_MEDIA_TYPE",
|
|
64
|
+
"MANIFEST_NOT_COMPILABLE",
|
|
65
|
+
"REFERENCE_HARNESS_ID",
|
|
66
|
+
"compile_plans",
|
|
67
|
+
"compile_variant_config",
|
|
68
|
+
"divide_concurrency",
|
|
69
|
+
"skill_directory_name",
|
|
70
|
+
"write_variant_config",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
#: Stable error code. Spec section 6.8.
|
|
74
|
+
MANIFEST_NOT_COMPILABLE: Final = "manifest_not_compilable"
|
|
75
|
+
|
|
76
|
+
#: The only subject harness WP6 executes. Spec section 6.2.
|
|
77
|
+
REFERENCE_HARNESS_ID: Final = "hermes-agent"
|
|
78
|
+
|
|
79
|
+
#: What Techtree writes for one variant. JSON rather than TOML because the
|
|
80
|
+
#: dashboard can only be turned off by an explicit null, and TOML has none
|
|
81
|
+
#: (``src/techtree/verifiers/config.py``, ``docs/verifiers-pin-0.3.1.md`` D5).
|
|
82
|
+
EVAL_CONFIG_MEDIA_TYPE: Final = "application/json"
|
|
83
|
+
|
|
84
|
+
#: The minimum Campaign-wide concurrency a parallel schedule can be divided
|
|
85
|
+
#: from: one permit each, and no variant may be starved to zero.
|
|
86
|
+
_MINIMUM_PARALLEL_CONCURRENCY: Final = 2
|
|
87
|
+
|
|
88
|
+
_VARIANTS: Final[dict[ExperimentVariant, VariantName]] = {
|
|
89
|
+
ExperimentVariant.BASELINE: VariantName.BASELINE,
|
|
90
|
+
ExperimentVariant.CANDIDATE: VariantName.CANDIDATE,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _refuse(message: str, **details: str | int | bool | None) -> NoReturn:
|
|
95
|
+
"""Raise the one error this module reports, with identifying details."""
|
|
96
|
+
raise ValidationError(
|
|
97
|
+
message,
|
|
98
|
+
code=MANIFEST_NOT_COMPILABLE,
|
|
99
|
+
details=dict(details),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def skill_directory_name(digest: Digest) -> str:
|
|
104
|
+
"""Return the run-owned directory name one skill's content tree occupies.
|
|
105
|
+
|
|
106
|
+
A skill is mounted by content, so its directory is named after its root
|
|
107
|
+
digest rather than after a human-chosen label. Hermes lands each folder at
|
|
108
|
+
``<skills dir>/<folder name>``, so the folder name is part of what the
|
|
109
|
+
subject sees and must therefore be a property of the skill rather than of
|
|
110
|
+
whoever prepared it.
|
|
111
|
+
"""
|
|
112
|
+
return digest.replace(":", "-", 1)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
# Compilation
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def compile_variant_config(
|
|
121
|
+
*,
|
|
122
|
+
campaign: CampaignSpec,
|
|
123
|
+
experiment: ExperimentManifest,
|
|
124
|
+
run_paths: RunPaths,
|
|
125
|
+
variant: VariantName,
|
|
126
|
+
variant_max_concurrent: int,
|
|
127
|
+
) -> EvalToml:
|
|
128
|
+
"""Translate one resolved Techtree experiment into the strict config.
|
|
129
|
+
|
|
130
|
+
Every disagreement between the manifest and the Campaign it claims to
|
|
131
|
+
derive from is refused here, before any file is written, because a
|
|
132
|
+
compiled config is the last point at which the two documents can still be
|
|
133
|
+
compared cheaply.
|
|
134
|
+
"""
|
|
135
|
+
campaign_digest = digest_object(campaign)
|
|
136
|
+
_check_manifest_derives_from(experiment, campaign, campaign_digest)
|
|
137
|
+
_check_variant_matches(experiment, variant)
|
|
138
|
+
|
|
139
|
+
subject = _subject_of(experiment)
|
|
140
|
+
_check_subject_is_executable(subject)
|
|
141
|
+
skill_paths = _resolve_skill_paths(subject, run_paths)
|
|
142
|
+
|
|
143
|
+
# ``output_dir`` names the directory the engine *groups* runs under, not
|
|
144
|
+
# the directory this run writes into; the invocation's ``--run.name`` names
|
|
145
|
+
# that (deviation D2). The two are joined in exactly one place,
|
|
146
|
+
# :meth:`RunPaths.variant_output_dir`.
|
|
147
|
+
output_dir = run_paths.variant_output_group_dir(variant)
|
|
148
|
+
taskset = experiment.configuration.taskset
|
|
149
|
+
allow, block = egress_for(subject.runtime.network_policy)
|
|
150
|
+
|
|
151
|
+
# Every limit the Campaign declares, compiled into the place the engine
|
|
152
|
+
# reads it. A declared budget the engine never sees is decorative, and
|
|
153
|
+
# decision 0029 forbids one.
|
|
154
|
+
#
|
|
155
|
+
# Binding v0.1 interpretation: one Verifiers model turn is the supported
|
|
156
|
+
# Hermes model-call budget unit, so ``maximum_model_calls`` compiles to
|
|
157
|
+
# ``max_turns``. That mapping is VALID ONLY IF the turn-conformance check
|
|
158
|
+
# holds for the pinned profile — intercepted subject generations equal
|
|
159
|
+
# ``Trace.num_turns`` on every recorded canonical episode
|
|
160
|
+
# (``tools/verify_turn_conformance.py``, spec section 7). If it ever
|
|
161
|
+
# fails, the answer is a new ``maximum_turns`` field and
|
|
162
|
+
# ``maximum_model_calls`` left unsupported, never a silent false mapping.
|
|
163
|
+
#
|
|
164
|
+
# ``max_total_tokens`` is derived rather than declared: a Campaign exposes
|
|
165
|
+
# three publisher decisions (turns, input, output) and the total is their
|
|
166
|
+
# sum (decision 0029, resolution 1).
|
|
167
|
+
maximum_input = campaign.budgets.maximum_input_tokens
|
|
168
|
+
maximum_output = campaign.budgets.maximum_output_tokens
|
|
169
|
+
maximum_turns = campaign.budgets.maximum_model_calls
|
|
170
|
+
maximum_total = (
|
|
171
|
+
maximum_input + maximum_output
|
|
172
|
+
if maximum_input is not None and maximum_output is not None
|
|
173
|
+
else None
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if variant_max_concurrent < 1:
|
|
177
|
+
_refuse(
|
|
178
|
+
"a variant needs at least one concurrency permit",
|
|
179
|
+
variant=variant.value,
|
|
180
|
+
variant_max_concurrent=variant_max_concurrent,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return EvalToml(
|
|
184
|
+
model=subject.model.model_id,
|
|
185
|
+
client=EvalClientToml(api_key_var=subject.model.credential_env),
|
|
186
|
+
sampling=SamplingToml(
|
|
187
|
+
temperature=subject.sampling.temperature,
|
|
188
|
+
max_tokens=subject.sampling.max_tokens,
|
|
189
|
+
),
|
|
190
|
+
env=EnvToml(
|
|
191
|
+
taskset=TasksetToml(id=taskset.ref.id),
|
|
192
|
+
subject=SubjectAgentToml(
|
|
193
|
+
harness=HermesHarnessToml(
|
|
194
|
+
version=subject.harness.version,
|
|
195
|
+
skills=skill_paths,
|
|
196
|
+
),
|
|
197
|
+
runtime=DockerRuntimeToml(
|
|
198
|
+
image=subject.runtime.image,
|
|
199
|
+
allow=allow,
|
|
200
|
+
block=block,
|
|
201
|
+
cpu=subject.runtime.cpu,
|
|
202
|
+
memory=subject.runtime.memory_gb,
|
|
203
|
+
),
|
|
204
|
+
max_turns=maximum_turns,
|
|
205
|
+
max_input_tokens=maximum_input,
|
|
206
|
+
max_output_tokens=maximum_output,
|
|
207
|
+
max_total_tokens=maximum_total,
|
|
208
|
+
# The Campaign's ``timeout_seconds`` bounds ONE subject
|
|
209
|
+
# rollout, not the whole variant. The variant's own bound is
|
|
210
|
+
# the supervisor's hard deadline (decision 0029, layer B).
|
|
211
|
+
timeout=TimeoutToml(rollout=float(campaign.execution.timeout_seconds)),
|
|
212
|
+
),
|
|
213
|
+
max_concurrent_agents=1,
|
|
214
|
+
),
|
|
215
|
+
num_tasks=taskset.selection.num_tasks,
|
|
216
|
+
max_concurrent=variant_max_concurrent,
|
|
217
|
+
output_dir=str(output_dir),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _subject_of(experiment: ExperimentManifest) -> AgentSpec:
|
|
222
|
+
"""Return the manifest's subject agent, or refuse."""
|
|
223
|
+
subject = experiment.configuration.agents.get(SUBJECT_AGENT)
|
|
224
|
+
if subject is None:
|
|
225
|
+
_refuse(
|
|
226
|
+
f"an experiment configuration defines a {SUBJECT_AGENT!r} agent",
|
|
227
|
+
manifest_id=experiment.id,
|
|
228
|
+
)
|
|
229
|
+
return subject
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _check_manifest_derives_from(
|
|
233
|
+
experiment: ExperimentManifest,
|
|
234
|
+
campaign: CampaignSpec,
|
|
235
|
+
campaign_digest: Digest,
|
|
236
|
+
) -> None:
|
|
237
|
+
"""Refuse a manifest that is not this Campaign, fully resolved."""
|
|
238
|
+
if experiment.campaign_spec_digest != campaign_digest:
|
|
239
|
+
_refuse(
|
|
240
|
+
"the experiment manifest was derived from a different Campaign",
|
|
241
|
+
manifest_id=experiment.id,
|
|
242
|
+
declared=experiment.campaign_spec_digest,
|
|
243
|
+
actual=campaign_digest,
|
|
244
|
+
)
|
|
245
|
+
if digest_object(experiment.configuration) != experiment.configuration_digest:
|
|
246
|
+
_refuse(
|
|
247
|
+
"the experiment configuration does not hash to its own digest",
|
|
248
|
+
manifest_id=experiment.id,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
configuration = experiment.configuration
|
|
252
|
+
if configuration.data_policy_digest != campaign.data_policy_digest:
|
|
253
|
+
_refuse(
|
|
254
|
+
"the experiment and its Campaign disagree about the data policy",
|
|
255
|
+
manifest_id=experiment.id,
|
|
256
|
+
)
|
|
257
|
+
if configuration.evaluation_backend != campaign.evaluation_backend:
|
|
258
|
+
_refuse(
|
|
259
|
+
"the experiment and its Campaign disagree about the evaluation backend",
|
|
260
|
+
manifest_id=experiment.id,
|
|
261
|
+
)
|
|
262
|
+
if configuration.taskset.ref != campaign.taskset.ref:
|
|
263
|
+
_refuse(
|
|
264
|
+
"the experiment and its Campaign point at different tasksets",
|
|
265
|
+
manifest_id=experiment.id,
|
|
266
|
+
)
|
|
267
|
+
if configuration.taskset.membership != campaign.taskset.membership:
|
|
268
|
+
_refuse(
|
|
269
|
+
"the experiment and its Campaign commit different task membership",
|
|
270
|
+
manifest_id=experiment.id,
|
|
271
|
+
)
|
|
272
|
+
committed = len(configuration.taskset.membership.ordered_task_hashes)
|
|
273
|
+
if committed != configuration.taskset.selection.num_tasks:
|
|
274
|
+
_refuse(
|
|
275
|
+
"the experiment commits a task count its selection does not ask for",
|
|
276
|
+
manifest_id=experiment.id,
|
|
277
|
+
committed=committed,
|
|
278
|
+
selected=configuration.taskset.selection.num_tasks,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _check_variant_matches(
|
|
283
|
+
experiment: ExperimentManifest, variant: VariantName
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Refuse a manifest compiled under the other variant's name."""
|
|
286
|
+
if _VARIANTS[experiment.variant] is not variant:
|
|
287
|
+
_refuse(
|
|
288
|
+
"the experiment manifest describes the other variant",
|
|
289
|
+
manifest_id=experiment.id,
|
|
290
|
+
manifest_variant=experiment.variant.value,
|
|
291
|
+
requested=variant.value,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _check_subject_is_executable(subject: AgentSpec) -> None:
|
|
296
|
+
"""Refuse a subject WP6 has no way to run."""
|
|
297
|
+
if subject.harness.id != REFERENCE_HARNESS_ID:
|
|
298
|
+
_refuse(
|
|
299
|
+
f"WP6 executes the {REFERENCE_HARNESS_ID!r} harness only",
|
|
300
|
+
harness_id=subject.harness.id,
|
|
301
|
+
)
|
|
302
|
+
if subject.harness.use_bundled_skill:
|
|
303
|
+
_refuse(
|
|
304
|
+
"a bundled skill catalogue is an uncontrolled second difference",
|
|
305
|
+
harness_id=subject.harness.id,
|
|
306
|
+
)
|
|
307
|
+
if subject.runtime.type != "docker":
|
|
308
|
+
_refuse(
|
|
309
|
+
"the subject runtime is Docker",
|
|
310
|
+
runtime_type=subject.runtime.type,
|
|
311
|
+
)
|
|
312
|
+
if subject.trainable:
|
|
313
|
+
_refuse(
|
|
314
|
+
"the subject is evaluated, never trained",
|
|
315
|
+
harness_id=subject.harness.id,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _resolve_skill_paths(subject: AgentSpec, run_paths: RunPaths) -> list[str]:
|
|
320
|
+
"""Return the run-owned directory each declared skill is mounted from."""
|
|
321
|
+
paths: list[str] = []
|
|
322
|
+
for skill in subject.harness.skills:
|
|
323
|
+
directory = run_paths.skill_files_dir / skill_directory_name(skill.digest)
|
|
324
|
+
if not run_paths.owns(directory):
|
|
325
|
+
_refuse(
|
|
326
|
+
"a skill must be mounted from the run's own input tree",
|
|
327
|
+
path=str(directory),
|
|
328
|
+
)
|
|
329
|
+
paths.append(str(directory))
|
|
330
|
+
return paths
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
# ---------------------------------------------------------------------------
|
|
334
|
+
# Writing
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def write_variant_config(config: EvalToml, destination: Path) -> ArtifactRef:
|
|
339
|
+
"""Write deterministic JSON and return its artifact reference.
|
|
340
|
+
|
|
341
|
+
The write is exclusive: a compiled config is an input to exactly one run,
|
|
342
|
+
and silently overwriting one would make a re-read prove nothing about what
|
|
343
|
+
the child was actually handed.
|
|
344
|
+
"""
|
|
345
|
+
data = config_to_json_bytes(config)
|
|
346
|
+
ensure_private_directory(destination.parent)
|
|
347
|
+
with open_exclusive(destination) as handle:
|
|
348
|
+
handle.write(data)
|
|
349
|
+
handle.flush()
|
|
350
|
+
os.fsync(handle.fileno())
|
|
351
|
+
fsync_directory(destination.parent)
|
|
352
|
+
return ArtifactRef(
|
|
353
|
+
digest=sha256_digest_bytes(data),
|
|
354
|
+
media_type=EVAL_CONFIG_MEDIA_TYPE,
|
|
355
|
+
size=len(data),
|
|
356
|
+
relative_path=None,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# ---------------------------------------------------------------------------
|
|
361
|
+
# Planning both variants
|
|
362
|
+
# ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def divide_concurrency(
|
|
366
|
+
schedule: VariantSchedule, max_concurrent: int
|
|
367
|
+
) -> tuple[int, int]:
|
|
368
|
+
"""Split the Campaign-wide concurrency bound between the two variants.
|
|
369
|
+
|
|
370
|
+
Under a sequential schedule only one variant is ever running, so each may
|
|
371
|
+
use the whole declared allowance. Under a parallel schedule the bound is
|
|
372
|
+
Campaign-wide (spec section 3.2) and dividing it is the executor's job:
|
|
373
|
+
granting each side the full allowance would double the live subject runs
|
|
374
|
+
the Campaign said it wanted.
|
|
375
|
+
"""
|
|
376
|
+
if max_concurrent < 1:
|
|
377
|
+
_refuse(
|
|
378
|
+
"a Campaign declares at least one concurrency permit",
|
|
379
|
+
max_concurrent=max_concurrent,
|
|
380
|
+
)
|
|
381
|
+
if schedule is VariantSchedule.SEQUENTIAL:
|
|
382
|
+
return max_concurrent, max_concurrent
|
|
383
|
+
if max_concurrent < _MINIMUM_PARALLEL_CONCURRENCY:
|
|
384
|
+
_refuse(
|
|
385
|
+
"a parallel schedule needs at least two concurrency permits, one "
|
|
386
|
+
"for each variant",
|
|
387
|
+
max_concurrent=max_concurrent,
|
|
388
|
+
)
|
|
389
|
+
baseline = max(1, math.floor(max_concurrent / 2))
|
|
390
|
+
return baseline, max(1, max_concurrent - baseline)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def compile_plans(
|
|
394
|
+
*,
|
|
395
|
+
campaign: CampaignSpec,
|
|
396
|
+
baseline: ExperimentManifest,
|
|
397
|
+
candidate: ExperimentManifest,
|
|
398
|
+
run_paths: RunPaths,
|
|
399
|
+
) -> tuple[VariantExecutionPlan, VariantExecutionPlan]:
|
|
400
|
+
"""Build both variants' plans and divide the Campaign's concurrency."""
|
|
401
|
+
schedule = campaign.execution.order
|
|
402
|
+
baseline_permits, candidate_permits = divide_concurrency(
|
|
403
|
+
schedule, campaign.execution.max_concurrent
|
|
404
|
+
)
|
|
405
|
+
manifests: Mapping[VariantName, tuple[ExperimentManifest, int]] = {
|
|
406
|
+
VariantName.BASELINE: (baseline, baseline_permits),
|
|
407
|
+
VariantName.CANDIDATE: (candidate, candidate_permits),
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
plans: dict[VariantName, VariantExecutionPlan] = {}
|
|
411
|
+
for variant, (manifest, permits) in manifests.items():
|
|
412
|
+
config = compile_variant_config(
|
|
413
|
+
campaign=campaign,
|
|
414
|
+
experiment=manifest,
|
|
415
|
+
run_paths=run_paths,
|
|
416
|
+
variant=variant,
|
|
417
|
+
variant_max_concurrent=permits,
|
|
418
|
+
)
|
|
419
|
+
plans[variant] = VariantExecutionPlan(
|
|
420
|
+
variant=variant,
|
|
421
|
+
experiment_manifest_digest=digest_object(manifest),
|
|
422
|
+
experiment_manifest_path=str(run_paths.manifest_path(variant)),
|
|
423
|
+
verifiers_input_config_path=str(run_paths.variant_input_config(variant)),
|
|
424
|
+
# The engine's ``--output-dir`` groups runs; the plan names the one
|
|
425
|
+
# this variant writes into, which is the group plus the name
|
|
426
|
+
# :func:`~techtree.verifiers.child.eval_argv` pins (deviation D2).
|
|
427
|
+
verifiers_output_dir=str(Path(config.output_dir) / EVAL_RUN_NAME),
|
|
428
|
+
skill_paths=list(config.env.subject.harness.skills),
|
|
429
|
+
task_count=config.num_tasks,
|
|
430
|
+
max_concurrent=config.max_concurrent,
|
|
431
|
+
)
|
|
432
|
+
return plans[VariantName.BASELINE], plans[VariantName.CANDIDATE]
|