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,698 @@
|
|
|
1
|
+
"""``techtree uplift context``, ``skill-source``, ``prepare``, ``start``.
|
|
2
|
+
|
|
3
|
+
Spec section 7.21. Four commands, and together they are the local half of the
|
|
4
|
+
improvement loop: say what a finished run showed, hand over the verified text
|
|
5
|
+
of the Skill it measured, set up the comparison between that Skill and a
|
|
6
|
+
revision of it, and start that comparison running.
|
|
7
|
+
|
|
8
|
+
``skill-source`` exists so that verification stays inside Techtree. Decisions
|
|
9
|
+
document 0007 R2 has a host agent read the run-owned snapshot of SKILL.md and
|
|
10
|
+
re-verify it; this is the surface it reads through, so nothing outside has to
|
|
11
|
+
compose a path into a run directory or reimplement the digest check.
|
|
12
|
+
|
|
13
|
+
What is deliberately *not* here is the revision itself. No command in this file
|
|
14
|
+
calls a model, asks anything to write a Skill, or reads one that a model wrote
|
|
15
|
+
without a person putting it there. ``uplift context`` produces the material a
|
|
16
|
+
host agent reasons over and stops; ``uplift prepare`` takes a directory a
|
|
17
|
+
person names and stops. Spec section 7.3 puts the reasoning turn in WP9+, and
|
|
18
|
+
the seam between the two is exactly this file's boundary.
|
|
19
|
+
|
|
20
|
+
Two things are kept as they are for public submissions, because the second run
|
|
21
|
+
is a real run and nothing about it is smaller.
|
|
22
|
+
|
|
23
|
+
*The rights policy is accepted again.* A second evaluation is a second use of
|
|
24
|
+
the participant's material, so ``uplift start`` collects approval the same way
|
|
25
|
+
``climb start`` does: the review of what the run would do is shown, the rights
|
|
26
|
+
summary is shown under it, and a person answers — or an operator who cannot be
|
|
27
|
+
asked passes ``--yes``. Spec section 7.20 requires it explicitly.
|
|
28
|
+
|
|
29
|
+
*The comparison is stated before it runs.* ``uplift prepare`` returns both
|
|
30
|
+
Skill digests, the derived Campaign digest, the estimated episodes, and the
|
|
31
|
+
DataPolicy digest, because that is what spec section 11.5 says a person must
|
|
32
|
+
see before approving a second run.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Annotated, Final, Literal
|
|
39
|
+
|
|
40
|
+
import typer
|
|
41
|
+
from pydantic import PositiveFloat
|
|
42
|
+
from rich.console import Console
|
|
43
|
+
from rich.table import Table
|
|
44
|
+
|
|
45
|
+
from techtree.canonical import canonical_json_bytes
|
|
46
|
+
from techtree.cli.commands.climb import (
|
|
47
|
+
PUBLICATION_TERMS_LINE,
|
|
48
|
+
ReviewSurface,
|
|
49
|
+
approve_run,
|
|
50
|
+
build_preparation_service,
|
|
51
|
+
phrase,
|
|
52
|
+
)
|
|
53
|
+
from techtree.cli.commands.run import build_run_service
|
|
54
|
+
from techtree.cli.context import CliContext, cli_context
|
|
55
|
+
from techtree.cli.invoke import CommandResult, invoke_command
|
|
56
|
+
from techtree.cli.output import render_pairs
|
|
57
|
+
from techtree.drafts.store import DraftStore
|
|
58
|
+
from techtree.fs import atomic_write_bytes, ensure_private_directory
|
|
59
|
+
from techtree.models.base import Digest, NonEmptyString, ProtocolModel
|
|
60
|
+
from techtree.models.cli import CliMessage, MessageLevel, NextAction
|
|
61
|
+
from techtree.models.run import RunPhase
|
|
62
|
+
from techtree.models.skill import PolicyAcceptanceRequirement
|
|
63
|
+
from techtree.paths import TechtreePaths
|
|
64
|
+
from techtree.runs.artifacts import RunArtifactStore
|
|
65
|
+
from techtree.runs.service import ApprovalActor
|
|
66
|
+
from techtree.skills.service import PreparedDraft
|
|
67
|
+
from techtree.uplift.context import SkillImprovementContext
|
|
68
|
+
from techtree.uplift.service import UpliftService
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
"CONTEXT_COMMAND",
|
|
72
|
+
"PREPARE_COMMAND",
|
|
73
|
+
"SKILL_SOURCE_COMMAND",
|
|
74
|
+
"START_COMMAND",
|
|
75
|
+
"UpliftContextPayload",
|
|
76
|
+
"UpliftPreparePayload",
|
|
77
|
+
"UpliftSkillSourcePayload",
|
|
78
|
+
"UpliftStartPayload",
|
|
79
|
+
"build_uplift_service",
|
|
80
|
+
"context_uplift_command",
|
|
81
|
+
"prepare_uplift_command",
|
|
82
|
+
"skill_source_uplift_command",
|
|
83
|
+
"start_uplift_command",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
CONTEXT_COMMAND: Final = "uplift context"
|
|
87
|
+
SKILL_SOURCE_COMMAND: Final = "uplift skill-source"
|
|
88
|
+
PREPARE_COMMAND: Final = "uplift prepare"
|
|
89
|
+
START_COMMAND: Final = "uplift start"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class UpliftContextPayload(ProtocolModel):
|
|
93
|
+
"""The sanitized context, and where the same bytes were written."""
|
|
94
|
+
|
|
95
|
+
context: SkillImprovementContext
|
|
96
|
+
relative_path: NonEmptyString
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class UpliftSkillSourcePayload(ProtocolModel):
|
|
100
|
+
"""The verified text of the Skill a run measured, with its fingerprints.
|
|
101
|
+
|
|
102
|
+
The digests are repeated beside the text on purpose. A caller that reads
|
|
103
|
+
this payload has everything the improvement context pins, so it can check
|
|
104
|
+
that the text it is about to send to a model belongs to the run the
|
|
105
|
+
context describes without resolving anything itself.
|
|
106
|
+
|
|
107
|
+
``entrypoint_text`` is a plain string rather than a non-empty one. What
|
|
108
|
+
makes it legitimate is that it hashes to a digest the run measured, and a
|
|
109
|
+
response shape that second-guessed verified bytes would turn a Skill
|
|
110
|
+
somebody really ran into an internal error rather than into an answer.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
source_run_id: NonEmptyString
|
|
114
|
+
skill_name: NonEmptyString
|
|
115
|
+
skill_root_digest: Digest
|
|
116
|
+
entrypoint_path: NonEmptyString
|
|
117
|
+
entrypoint_digest: Digest
|
|
118
|
+
entrypoint_size: int
|
|
119
|
+
entrypoint_text: str
|
|
120
|
+
file_count: int
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class UpliftPreparePayload(ProtocolModel):
|
|
124
|
+
"""What ``uplift prepare`` returns: the draft, and what it commits to.
|
|
125
|
+
|
|
126
|
+
It discloses what ``climb prepare`` discloses. Decisions document 0019
|
|
127
|
+
section 1 makes the two comparisons the same kind of thing — one Skill
|
|
128
|
+
replaced by another, one Skill added where there was none — so the screen a
|
|
129
|
+
person approves the second run from states the same facts as the screen
|
|
130
|
+
they approved the first from: how many Skills each side carries, and which
|
|
131
|
+
data rights govern what this run produces.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
draft_id: NonEmptyString
|
|
135
|
+
draft_digest: Digest
|
|
136
|
+
source_run_id: NonEmptyString
|
|
137
|
+
campaign_spec_digest: Digest
|
|
138
|
+
data_policy_digest: Digest
|
|
139
|
+
baseline_skill_digest: Digest
|
|
140
|
+
candidate_skill_digest: Digest
|
|
141
|
+
candidate_label: NonEmptyString
|
|
142
|
+
included_files: list[NonEmptyString]
|
|
143
|
+
baseline_skill_count: int
|
|
144
|
+
candidate_skill_count: int
|
|
145
|
+
estimated_episodes: int
|
|
146
|
+
# Read off the Campaign this replacement was prepared against, so a caller
|
|
147
|
+
# that renders a review shows the maximum this second run is held to and
|
|
148
|
+
# never a figure from somewhere else. ``None`` is a Campaign that declares
|
|
149
|
+
# no maximum, and says so: there is then no figure to hold it to. Decision
|
|
150
|
+
# 0019 section 2 asks the same of both comparisons, and ``climb prepare``
|
|
151
|
+
# already carries it for the first.
|
|
152
|
+
campaign_maximum_usd: PositiveFloat | None
|
|
153
|
+
candidate_ownership: Literal["participant", "account", "shared"]
|
|
154
|
+
candidate_public_release: Literal[
|
|
155
|
+
"required_for_climb", "allowed", "prohibited", "consent_required"
|
|
156
|
+
]
|
|
157
|
+
raw_episode_server_upload: Literal["allowed", "prohibited", "consent_required"]
|
|
158
|
+
raw_episode_training_use: Literal["allowed", "prohibited", "consent_required"]
|
|
159
|
+
controlled: bool
|
|
160
|
+
allowed_differences: list[NonEmptyString]
|
|
161
|
+
differences: list[NonEmptyString]
|
|
162
|
+
policy_acceptance: PolicyAcceptanceRequirement
|
|
163
|
+
warnings: list[NonEmptyString]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class UpliftStartPayload(ProtocolModel):
|
|
167
|
+
"""What ``uplift start`` returns, as soon as the worker is running."""
|
|
168
|
+
|
|
169
|
+
run_id: NonEmptyString
|
|
170
|
+
draft_id: NonEmptyString
|
|
171
|
+
draft_digest: Digest
|
|
172
|
+
phase: RunPhase
|
|
173
|
+
worker_pid: int | None
|
|
174
|
+
campaign_spec_digest: Digest
|
|
175
|
+
data_policy_digest: Digest
|
|
176
|
+
policy_acknowledgement_method: Literal[
|
|
177
|
+
"explicit_cli_review",
|
|
178
|
+
"host_agent_confirmation",
|
|
179
|
+
]
|
|
180
|
+
approved_by: ApprovalActor
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def build_uplift_service(context: CliContext) -> UpliftService:
|
|
184
|
+
"""Construct the service the improvement commands act through."""
|
|
185
|
+
return UpliftService(
|
|
186
|
+
paths=context.paths,
|
|
187
|
+
run_service=build_run_service(context),
|
|
188
|
+
artifact_store=RunArtifactStore(context.paths),
|
|
189
|
+
skill_service=build_preparation_service(context),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
# context
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def context_uplift_command(
|
|
199
|
+
ctx: typer.Context,
|
|
200
|
+
run_id: Annotated[
|
|
201
|
+
str,
|
|
202
|
+
typer.Argument(
|
|
203
|
+
metavar="RUN_ID",
|
|
204
|
+
help="The finished run to build improvement context from.",
|
|
205
|
+
),
|
|
206
|
+
],
|
|
207
|
+
) -> None:
|
|
208
|
+
"""Export the sanitized local context for one finished run."""
|
|
209
|
+
context = cli_context(ctx)
|
|
210
|
+
|
|
211
|
+
def action() -> CommandResult[UpliftContextPayload]:
|
|
212
|
+
improvement = build_uplift_service(context).improvement_context(run_id)
|
|
213
|
+
path = _write_context(context.paths, run_id, improvement)
|
|
214
|
+
payload = UpliftContextPayload(
|
|
215
|
+
context=improvement,
|
|
216
|
+
relative_path=path.relative_to(context.paths.run_dir(run_id)).as_posix(),
|
|
217
|
+
)
|
|
218
|
+
return CommandResult(
|
|
219
|
+
data=payload,
|
|
220
|
+
messages=[
|
|
221
|
+
CliMessage(
|
|
222
|
+
level=MessageLevel.INFO,
|
|
223
|
+
code="improvement_context_ready",
|
|
224
|
+
text=(
|
|
225
|
+
f"Built improvement context for {run_id} from "
|
|
226
|
+
f"{len(improvement.examples)} of this run's tasks."
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
],
|
|
230
|
+
warnings=[
|
|
231
|
+
CliMessage(
|
|
232
|
+
level=MessageLevel.WARNING,
|
|
233
|
+
code="improvement_context_is_not_proof",
|
|
234
|
+
text=(
|
|
235
|
+
"This context is working material, not evidence. It is "
|
|
236
|
+
"not signed, nothing verifies it, and nothing uploads it."
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
],
|
|
240
|
+
next_actions=[_prepare_replacement(run_id)],
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
invoke_command(context, CONTEXT_COMMAND, action, render_data=_render_context)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _write_context(
|
|
247
|
+
paths: TechtreePaths, run_id: str, context: SkillImprovementContext
|
|
248
|
+
) -> Path:
|
|
249
|
+
"""Write the context beside the run it describes, replacing any older one.
|
|
250
|
+
|
|
251
|
+
A run can be revised from more than once, and the context is derived rather
|
|
252
|
+
than evidence, so it is a file that may be rewritten — unlike everything
|
|
253
|
+
the run's proof covers, which is written exactly once.
|
|
254
|
+
"""
|
|
255
|
+
directory = paths.run_dir(run_id) / "improvement"
|
|
256
|
+
ensure_private_directory(directory)
|
|
257
|
+
path = directory / "context.json"
|
|
258
|
+
atomic_write_bytes(path, canonical_json_bytes(context), mode=0o600)
|
|
259
|
+
return path
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
# skill-source
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def skill_source_uplift_command(
|
|
268
|
+
ctx: typer.Context,
|
|
269
|
+
run_id: Annotated[
|
|
270
|
+
str,
|
|
271
|
+
typer.Argument(
|
|
272
|
+
metavar="RUN_ID",
|
|
273
|
+
help="The finished run whose Skill text to read.",
|
|
274
|
+
),
|
|
275
|
+
],
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Show the verified text of the Skill a finished run measured."""
|
|
278
|
+
context = cli_context(ctx)
|
|
279
|
+
|
|
280
|
+
def action() -> CommandResult[UpliftSkillSourcePayload]:
|
|
281
|
+
skill = build_uplift_service(context).verified_source_skill(run_id)
|
|
282
|
+
payload = UpliftSkillSourcePayload(
|
|
283
|
+
source_run_id=skill.run_id,
|
|
284
|
+
skill_name=skill.name,
|
|
285
|
+
skill_root_digest=skill.root_digest,
|
|
286
|
+
entrypoint_path=skill.entrypoint_path,
|
|
287
|
+
entrypoint_digest=skill.entrypoint_digest,
|
|
288
|
+
entrypoint_size=skill.entrypoint_size,
|
|
289
|
+
entrypoint_text=skill.entrypoint_text,
|
|
290
|
+
file_count=skill.file_count,
|
|
291
|
+
)
|
|
292
|
+
return CommandResult(
|
|
293
|
+
data=payload,
|
|
294
|
+
messages=[
|
|
295
|
+
CliMessage(
|
|
296
|
+
level=MessageLevel.INFO,
|
|
297
|
+
code="source_skill_verified",
|
|
298
|
+
text=(
|
|
299
|
+
f"This is run {run_id}'s own copy of "
|
|
300
|
+
f"{payload.entrypoint_path}, re-verified against the "
|
|
301
|
+
"Skill the run measured as it was read."
|
|
302
|
+
),
|
|
303
|
+
)
|
|
304
|
+
],
|
|
305
|
+
next_actions=[_prepare_replacement(run_id)],
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
invoke_command(
|
|
309
|
+
context, SKILL_SOURCE_COMMAND, action, render_data=_render_skill_source
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
# prepare
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def prepare_uplift_command(
|
|
319
|
+
ctx: typer.Context,
|
|
320
|
+
from_run: Annotated[
|
|
321
|
+
str,
|
|
322
|
+
typer.Option(
|
|
323
|
+
"--from-run",
|
|
324
|
+
metavar="RUN_ID",
|
|
325
|
+
help="The finished run whose Skill becomes the new baseline.",
|
|
326
|
+
),
|
|
327
|
+
],
|
|
328
|
+
candidate_skill: Annotated[
|
|
329
|
+
Path,
|
|
330
|
+
typer.Option(
|
|
331
|
+
"--candidate-skill",
|
|
332
|
+
metavar="PATH",
|
|
333
|
+
help="The revised skill directory, or its SKILL.md.",
|
|
334
|
+
),
|
|
335
|
+
],
|
|
336
|
+
label: Annotated[
|
|
337
|
+
str | None,
|
|
338
|
+
typer.Option(
|
|
339
|
+
"--label",
|
|
340
|
+
metavar="LABEL",
|
|
341
|
+
help="What to call the revision. Defaults to the directory name.",
|
|
342
|
+
),
|
|
343
|
+
] = None,
|
|
344
|
+
) -> None:
|
|
345
|
+
"""Prepare a Skill v1 against Skill v2 comparison from a finished run."""
|
|
346
|
+
context = cli_context(ctx)
|
|
347
|
+
|
|
348
|
+
def action() -> CommandResult[UpliftPreparePayload]:
|
|
349
|
+
prepared = build_uplift_service(context).prepare_replacement(
|
|
350
|
+
source_run_id=from_run,
|
|
351
|
+
candidate_skill_path=candidate_skill,
|
|
352
|
+
candidate_label=label,
|
|
353
|
+
)
|
|
354
|
+
payload = _prepare_payload(from_run, prepared)
|
|
355
|
+
return CommandResult(
|
|
356
|
+
data=payload,
|
|
357
|
+
messages=[
|
|
358
|
+
CliMessage(
|
|
359
|
+
level=MessageLevel.INFO,
|
|
360
|
+
code="replacement_prepared",
|
|
361
|
+
text=(
|
|
362
|
+
f"Prepared {payload.candidate_label} against the Skill "
|
|
363
|
+
f"run {from_run} measured. Nothing has run yet."
|
|
364
|
+
),
|
|
365
|
+
)
|
|
366
|
+
],
|
|
367
|
+
warnings=[
|
|
368
|
+
CliMessage(
|
|
369
|
+
level=MessageLevel.WARNING,
|
|
370
|
+
code="draft_warning",
|
|
371
|
+
text=warning,
|
|
372
|
+
)
|
|
373
|
+
for warning in payload.warnings
|
|
374
|
+
],
|
|
375
|
+
next_actions=[_start_replacement(payload)],
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
invoke_command(context, PREPARE_COMMAND, action, render_data=_render_prepare)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _prepare_payload(
|
|
382
|
+
source_run_id: str, prepared: PreparedDraft
|
|
383
|
+
) -> UpliftPreparePayload:
|
|
384
|
+
"""Project a prepared replacement draft into the response a caller acts on."""
|
|
385
|
+
draft = prepared.draft
|
|
386
|
+
comparison = prepared.manifest_comparison
|
|
387
|
+
campaign = prepared.source.campaign
|
|
388
|
+
subject = campaign.subject
|
|
389
|
+
data_policy = prepared.source.data_policy
|
|
390
|
+
return UpliftPreparePayload(
|
|
391
|
+
draft_id=draft.id,
|
|
392
|
+
draft_digest=prepared.draft_digest,
|
|
393
|
+
source_run_id=source_run_id,
|
|
394
|
+
campaign_spec_digest=draft.campaign_spec_digest,
|
|
395
|
+
data_policy_digest=draft.data_policy_digest,
|
|
396
|
+
baseline_skill_digest=subject.harness.skills[0].digest,
|
|
397
|
+
candidate_skill_digest=draft.skill_artifact.root_digest,
|
|
398
|
+
candidate_label=draft.skill_artifact.name,
|
|
399
|
+
included_files=list(draft.included_files),
|
|
400
|
+
baseline_skill_count=len(subject.harness.skills),
|
|
401
|
+
candidate_skill_count=1,
|
|
402
|
+
estimated_episodes=draft.estimated_episodes,
|
|
403
|
+
campaign_maximum_usd=campaign.budgets.maximum_usd,
|
|
404
|
+
candidate_ownership=data_policy.candidate_skill.ownership,
|
|
405
|
+
candidate_public_release=data_policy.candidate_skill.public_release,
|
|
406
|
+
raw_episode_server_upload=data_policy.raw_episodes.server_upload,
|
|
407
|
+
raw_episode_training_use=data_policy.raw_episodes.training_use,
|
|
408
|
+
controlled=comparison.controlled,
|
|
409
|
+
allowed_differences=list(comparison.allowed_differences),
|
|
410
|
+
differences=[difference.pointer for difference in comparison.differences],
|
|
411
|
+
policy_acceptance=draft.policy_acceptance,
|
|
412
|
+
warnings=list(draft.warnings),
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# ---------------------------------------------------------------------------
|
|
417
|
+
# start
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def start_uplift_command(
|
|
422
|
+
ctx: typer.Context,
|
|
423
|
+
draft_id: Annotated[
|
|
424
|
+
str,
|
|
425
|
+
typer.Argument(metavar="DRAFT_ID", help="The prepared replacement to start."),
|
|
426
|
+
],
|
|
427
|
+
yes: Annotated[
|
|
428
|
+
bool,
|
|
429
|
+
typer.Option(
|
|
430
|
+
"--yes",
|
|
431
|
+
help=(
|
|
432
|
+
"Approve this run without being asked. For an operator running "
|
|
433
|
+
"Techtree where nobody can answer a prompt; it is never a "
|
|
434
|
+
"shortcut for an agent to take on a person's behalf."
|
|
435
|
+
),
|
|
436
|
+
),
|
|
437
|
+
] = False,
|
|
438
|
+
reviewed_on: Annotated[
|
|
439
|
+
ReviewSurface,
|
|
440
|
+
typer.Option(
|
|
441
|
+
"--reviewed-on",
|
|
442
|
+
help=(
|
|
443
|
+
"Where the person who approved this run answered. Pass "
|
|
444
|
+
"host-agent when the review was shown in a conversation and "
|
|
445
|
+
"confirmed there before the run was dispatched, so the run "
|
|
446
|
+
"records the surface the answer was actually given on. Like "
|
|
447
|
+
"--yes, and for the same reason, it states what a person "
|
|
448
|
+
"already did and is never a shortcut a model may take."
|
|
449
|
+
),
|
|
450
|
+
),
|
|
451
|
+
] = ReviewSurface.CLI,
|
|
452
|
+
) -> None:
|
|
453
|
+
"""Review the prepared revision, approve it, and start the second run."""
|
|
454
|
+
context = cli_context(ctx)
|
|
455
|
+
|
|
456
|
+
def action() -> CommandResult[UpliftStartPayload]:
|
|
457
|
+
service = build_run_service(context)
|
|
458
|
+
store = DraftStore(context.paths)
|
|
459
|
+
draft = store.get(draft_id)
|
|
460
|
+
approval = approve_run(
|
|
461
|
+
context,
|
|
462
|
+
draft=draft,
|
|
463
|
+
campaign=store.get_source(draft_id).campaign,
|
|
464
|
+
assume_yes=yes,
|
|
465
|
+
reviewed_on=reviewed_on,
|
|
466
|
+
)
|
|
467
|
+
status = service.start(
|
|
468
|
+
draft_id=draft_id,
|
|
469
|
+
policy_acknowledgement=approval.acknowledgement,
|
|
470
|
+
approved_by=approval.actor,
|
|
471
|
+
)
|
|
472
|
+
request = service.request(status.state.run_id)
|
|
473
|
+
payload = UpliftStartPayload(
|
|
474
|
+
run_id=status.state.run_id,
|
|
475
|
+
draft_id=draft.id,
|
|
476
|
+
draft_digest=request.draft_digest,
|
|
477
|
+
phase=status.state.phase,
|
|
478
|
+
worker_pid=status.state.worker_pid,
|
|
479
|
+
campaign_spec_digest=draft.campaign_spec_digest,
|
|
480
|
+
data_policy_digest=draft.data_policy_digest,
|
|
481
|
+
policy_acknowledgement_method=approval.acknowledgement.method,
|
|
482
|
+
approved_by=approval.actor,
|
|
483
|
+
)
|
|
484
|
+
return CommandResult(
|
|
485
|
+
data=payload,
|
|
486
|
+
messages=[
|
|
487
|
+
CliMessage(
|
|
488
|
+
level=MessageLevel.INFO,
|
|
489
|
+
code="run_started",
|
|
490
|
+
text=(
|
|
491
|
+
f"Run {payload.run_id} is going. It continues whether "
|
|
492
|
+
"or not this command is still open."
|
|
493
|
+
),
|
|
494
|
+
)
|
|
495
|
+
],
|
|
496
|
+
next_actions=[_watch_run(payload.run_id)],
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
invoke_command(context, START_COMMAND, action, render_data=_render_start)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# ---------------------------------------------------------------------------
|
|
503
|
+
# Next actions
|
|
504
|
+
# ---------------------------------------------------------------------------
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _prepare_replacement(run_id: str) -> NextAction:
|
|
508
|
+
return NextAction(
|
|
509
|
+
id="prepare_replacement",
|
|
510
|
+
label="Compare a revised Skill against the one this run measured",
|
|
511
|
+
reason=(
|
|
512
|
+
"Point --candidate-skill at the revised skill directory. The "
|
|
513
|
+
"baseline is pinned to the Skill this run actually evaluated."
|
|
514
|
+
),
|
|
515
|
+
cli=[
|
|
516
|
+
"techtree",
|
|
517
|
+
"uplift",
|
|
518
|
+
"prepare",
|
|
519
|
+
"--from-run",
|
|
520
|
+
run_id,
|
|
521
|
+
"--candidate-skill",
|
|
522
|
+
"PATH",
|
|
523
|
+
],
|
|
524
|
+
hermes_tool=None,
|
|
525
|
+
hermes_args=None,
|
|
526
|
+
requires_user_confirmation=True,
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _start_replacement(payload: UpliftPreparePayload) -> NextAction:
|
|
531
|
+
return NextAction(
|
|
532
|
+
id="start_replacement",
|
|
533
|
+
label=f"Start {payload.candidate_label} against the previous Skill",
|
|
534
|
+
reason=(
|
|
535
|
+
f"Runs {payload.estimated_episodes} episodes. It shows you the "
|
|
536
|
+
"spending limit the Campaign declares and what this changes, and "
|
|
537
|
+
"starts only if you say yes."
|
|
538
|
+
),
|
|
539
|
+
cli=["techtree", "uplift", "start", payload.draft_id],
|
|
540
|
+
hermes_tool=None,
|
|
541
|
+
hermes_args=None,
|
|
542
|
+
requires_user_confirmation=True,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def _watch_run(run_id: str) -> NextAction:
|
|
547
|
+
return NextAction(
|
|
548
|
+
id="run_status",
|
|
549
|
+
label="Check how the run is going",
|
|
550
|
+
reason="The run continues after this command returns.",
|
|
551
|
+
cli=["techtree", "run", "status", run_id],
|
|
552
|
+
hermes_tool=None,
|
|
553
|
+
hermes_args=None,
|
|
554
|
+
requires_user_confirmation=False,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# ---------------------------------------------------------------------------
|
|
559
|
+
# Human rendering
|
|
560
|
+
# ---------------------------------------------------------------------------
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _render_context(data: object, console: Console) -> None:
|
|
564
|
+
"""Print a short summary and where the full context was written."""
|
|
565
|
+
if not isinstance(data, UpliftContextPayload):
|
|
566
|
+
return
|
|
567
|
+
improvement = data.context
|
|
568
|
+
|
|
569
|
+
render_pairs(
|
|
570
|
+
[
|
|
571
|
+
("Run", improvement.source_run_id),
|
|
572
|
+
("Skill being revised", improvement.parent_skill_digest),
|
|
573
|
+
("Tasks included", str(len(improvement.examples))),
|
|
574
|
+
("Written to", data.relative_path),
|
|
575
|
+
],
|
|
576
|
+
console,
|
|
577
|
+
)
|
|
578
|
+
console.print()
|
|
579
|
+
console.print(improvement.objective)
|
|
580
|
+
|
|
581
|
+
console.print()
|
|
582
|
+
console.print("Tasks worth looking at")
|
|
583
|
+
table = Table(box=None, pad_edge=False, padding=(0, 2))
|
|
584
|
+
table.add_column("Task", no_wrap=True)
|
|
585
|
+
table.add_column("Outcome", no_wrap=True)
|
|
586
|
+
table.add_column("Reward", justify="right", no_wrap=True)
|
|
587
|
+
for example in improvement.examples:
|
|
588
|
+
table.add_row(
|
|
589
|
+
example.task_label,
|
|
590
|
+
example.outcome.replace("_", " "),
|
|
591
|
+
f"{example.reward:.3f}",
|
|
592
|
+
)
|
|
593
|
+
console.print(table)
|
|
594
|
+
|
|
595
|
+
console.print()
|
|
596
|
+
console.print("Not included")
|
|
597
|
+
for item in improvement.prohibited_material:
|
|
598
|
+
console.print(f" {item}")
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _render_skill_source(data: object, console: Console) -> None:
|
|
602
|
+
"""Print what the text was verified against, then the text itself."""
|
|
603
|
+
if not isinstance(data, UpliftSkillSourcePayload):
|
|
604
|
+
return
|
|
605
|
+
|
|
606
|
+
render_pairs(
|
|
607
|
+
[
|
|
608
|
+
("Run", data.source_run_id),
|
|
609
|
+
("Skill", data.skill_name),
|
|
610
|
+
("Skill content digest", data.skill_root_digest),
|
|
611
|
+
("Entry file", data.entrypoint_path),
|
|
612
|
+
("Entry file digest", data.entrypoint_digest),
|
|
613
|
+
("Files in this Skill", str(data.file_count)),
|
|
614
|
+
],
|
|
615
|
+
console,
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
console.print()
|
|
619
|
+
console.print(f"{data.entrypoint_path} ({data.entrypoint_size} bytes)")
|
|
620
|
+
console.print()
|
|
621
|
+
console.print(data.entrypoint_text)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _render_prepare(data: object, console: Console) -> None:
|
|
625
|
+
"""Print everything a person needs before approving a second run."""
|
|
626
|
+
if not isinstance(data, UpliftPreparePayload):
|
|
627
|
+
return
|
|
628
|
+
|
|
629
|
+
render_pairs(
|
|
630
|
+
[
|
|
631
|
+
("Draft", data.draft_id),
|
|
632
|
+
("From run", data.source_run_id),
|
|
633
|
+
("Campaign digest", data.campaign_spec_digest),
|
|
634
|
+
("Data policy digest", data.data_policy_digest),
|
|
635
|
+
("Baseline skill", data.baseline_skill_digest),
|
|
636
|
+
("Candidate skill", data.candidate_skill_digest),
|
|
637
|
+
("Candidate", data.candidate_label),
|
|
638
|
+
],
|
|
639
|
+
console,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
console.print()
|
|
643
|
+
console.print(f"Included files ({len(data.included_files)})")
|
|
644
|
+
for path in data.included_files:
|
|
645
|
+
console.print(f" {path}")
|
|
646
|
+
|
|
647
|
+
console.print()
|
|
648
|
+
console.print("The comparison")
|
|
649
|
+
render_pairs(
|
|
650
|
+
[
|
|
651
|
+
("Allowed difference", ", ".join(data.allowed_differences)),
|
|
652
|
+
("Found difference", ", ".join(data.differences)),
|
|
653
|
+
("Baseline skills", str(data.baseline_skill_count)),
|
|
654
|
+
("Candidate skills", str(data.candidate_skill_count)),
|
|
655
|
+
("Controlled", "yes" if data.controlled else "no"),
|
|
656
|
+
("Estimated episodes", str(data.estimated_episodes)),
|
|
657
|
+
],
|
|
658
|
+
console,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
console.print()
|
|
662
|
+
console.print("Data rights")
|
|
663
|
+
render_pairs(
|
|
664
|
+
[
|
|
665
|
+
("Candidate ownership", data.candidate_ownership),
|
|
666
|
+
("Public release", phrase(data.candidate_public_release)),
|
|
667
|
+
("Raw episode upload", phrase(data.raw_episode_server_upload)),
|
|
668
|
+
("Training use", phrase(data.raw_episode_training_use)),
|
|
669
|
+
(
|
|
670
|
+
"Acceptance",
|
|
671
|
+
"required before starting"
|
|
672
|
+
if data.policy_acceptance.required
|
|
673
|
+
else "not required",
|
|
674
|
+
),
|
|
675
|
+
],
|
|
676
|
+
console,
|
|
677
|
+
)
|
|
678
|
+
console.print(data.policy_acceptance.summary)
|
|
679
|
+
console.print(PUBLICATION_TERMS_LINE)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _render_start(data: object, console: Console) -> None:
|
|
683
|
+
"""Print what was started and where it can be followed."""
|
|
684
|
+
if not isinstance(data, UpliftStartPayload):
|
|
685
|
+
return
|
|
686
|
+
render_pairs(
|
|
687
|
+
[
|
|
688
|
+
("Run", data.run_id),
|
|
689
|
+
("Draft", data.draft_id),
|
|
690
|
+
("Phase", data.phase.value),
|
|
691
|
+
("Worker", "not started" if data.worker_pid is None else "running"),
|
|
692
|
+
("Campaign digest", data.campaign_spec_digest),
|
|
693
|
+
("Data policy digest", data.data_policy_digest),
|
|
694
|
+
("Approved", phrase(data.policy_acknowledgement_method)),
|
|
695
|
+
("Approved by", phrase(data.approved_by)),
|
|
696
|
+
],
|
|
697
|
+
console,
|
|
698
|
+
)
|