world-model-optimizer 0.2.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.
- llm_waterfall/LICENSE +21 -0
- llm_waterfall/__init__.py +53 -0
- llm_waterfall/adapters/__init__.py +36 -0
- llm_waterfall/adapters/anthropic.py +105 -0
- llm_waterfall/adapters/aws_mantle.py +47 -0
- llm_waterfall/adapters/azure_openai.py +71 -0
- llm_waterfall/adapters/base.py +51 -0
- llm_waterfall/adapters/bedrock.py +309 -0
- llm_waterfall/adapters/openai.py +130 -0
- llm_waterfall/classify.py +184 -0
- llm_waterfall/pricing.py +110 -0
- llm_waterfall/py.typed +0 -0
- llm_waterfall/types.py +295 -0
- llm_waterfall/waterfall.py +255 -0
- wmo/__init__.py +38 -0
- wmo/agents/__init__.py +7 -0
- wmo/agents/default.py +29 -0
- wmo/agents/meta.py +55 -0
- wmo/agents/optimizer.py +55 -0
- wmo/agents/project.py +928 -0
- wmo/cli/__init__.py +5 -0
- wmo/cli/agent_session.py +1123 -0
- wmo/cli/app.py +2489 -0
- wmo/cli/e2b_cmds.py +212 -0
- wmo/cli/eval_closed_loop.py +207 -0
- wmo/cli/harness_app.py +1147 -0
- wmo/cli/harness_distill.py +659 -0
- wmo/cli/hosted_session.py +880 -0
- wmo/cli/ingest_cmd.py +165 -0
- wmo/cli/model_roles.py +82 -0
- wmo/cli/platform_cmds.py +372 -0
- wmo/cli/route_app.py +274 -0
- wmo/cli/session_state.py +243 -0
- wmo/cli/ui.py +1107 -0
- wmo/cli/workspace_sync.py +504 -0
- wmo/config/__init__.py +60 -0
- wmo/config/card.py +129 -0
- wmo/config/config.py +367 -0
- wmo/config/dotenv.py +67 -0
- wmo/config/settings.py +128 -0
- wmo/config/store.py +177 -0
- wmo/conftest.py +19 -0
- wmo/connect/__init__.py +88 -0
- wmo/connect/apps.py +78 -0
- wmo/connect/brave.py +284 -0
- wmo/connect/connector.py +79 -0
- wmo/connect/credentials.py +164 -0
- wmo/connect/github.py +321 -0
- wmo/connect/google.py +627 -0
- wmo/connect/notion.py +790 -0
- wmo/connect/oauth.py +461 -0
- wmo/connect/slack.py +555 -0
- wmo/connect/store.py +199 -0
- wmo/connect/types.py +156 -0
- wmo/core/__init__.py +21 -0
- wmo/core/parsing.py +281 -0
- wmo/core/render.py +271 -0
- wmo/core/text.py +40 -0
- wmo/core/types.py +116 -0
- wmo/distill/__init__.py +14 -0
- wmo/distill/agents.py +140 -0
- wmo/distill/config.py +1006 -0
- wmo/distill/cost.py +437 -0
- wmo/distill/data.py +921 -0
- wmo/distill/deadlines.py +254 -0
- wmo/distill/fake_tinker.py +734 -0
- wmo/distill/gate.py +122 -0
- wmo/distill/loop.py +3499 -0
- wmo/distill/renderers.py +399 -0
- wmo/distill/rendering.py +620 -0
- wmo/distill/rollouts.py +726 -0
- wmo/distill/samples.py +195 -0
- wmo/distill/store.py +829 -0
- wmo/distill/teacher.py +714 -0
- wmo/distill/tokens.py +535 -0
- wmo/distill/tracking.py +552 -0
- wmo/distill/tripwire.py +411 -0
- wmo/distill/xtoken/byte_offsets.py +152 -0
- wmo/distill/xtoken/chunks.py +457 -0
- wmo/distill/xtoken/prompt_logprobs.py +475 -0
- wmo/distill/xtoken/teacher_render.py +346 -0
- wmo/engine/__init__.py +28 -0
- wmo/engine/autoconfig.py +367 -0
- wmo/engine/build.py +346 -0
- wmo/engine/demo.py +77 -0
- wmo/engine/eval_suites.py +245 -0
- wmo/engine/grounding.py +491 -0
- wmo/engine/knowledge.py +291 -0
- wmo/engine/loader.py +36 -0
- wmo/engine/play.py +92 -0
- wmo/engine/prompts.py +99 -0
- wmo/engine/replay.py +443 -0
- wmo/engine/reporting.py +58 -0
- wmo/engine/workspace.py +468 -0
- wmo/engine/world_model.py +568 -0
- wmo/env/__init__.py +22 -0
- wmo/env/base.py +121 -0
- wmo/env/closed_loop.py +229 -0
- wmo/env/episode.py +107 -0
- wmo/env/llm_agent.py +93 -0
- wmo/env/scenarios.py +73 -0
- wmo/evals/__init__.py +52 -0
- wmo/evals/agreement.py +110 -0
- wmo/evals/base.py +45 -0
- wmo/evals/closed_loop.py +480 -0
- wmo/evals/failover.py +96 -0
- wmo/evals/gold.py +127 -0
- wmo/evals/grid.py +394 -0
- wmo/evals/grid_plot.py +205 -0
- wmo/evals/harbor/__init__.py +27 -0
- wmo/evals/harbor/agent.py +573 -0
- wmo/evals/harbor/ctrf.py +171 -0
- wmo/evals/harbor/e2b_environment.py +587 -0
- wmo/evals/harbor/e2b_template_policy.py +144 -0
- wmo/evals/harbor/scorer.py +875 -0
- wmo/evals/harbor/tasks.py +140 -0
- wmo/evals/open_loop.py +194 -0
- wmo/evals/tasks.py +53 -0
- wmo/harness/__init__.py +51 -0
- wmo/harness/code_runtime.py +288 -0
- wmo/harness/create.py +1191 -0
- wmo/harness/delta.py +220 -0
- wmo/harness/doc.py +556 -0
- wmo/harness/e2b_ledger.py +342 -0
- wmo/harness/e2b_reap.py +476 -0
- wmo/harness/e2b_sandbox.py +350 -0
- wmo/harness/environment.py +35 -0
- wmo/harness/live_session.py +543 -0
- wmo/harness/mutate.py +343 -0
- wmo/harness/pi_e2b.py +1710 -0
- wmo/harness/pi_entry/entry.ts +268 -0
- wmo/harness/pi_entry/runner_frames.ts +92 -0
- wmo/harness/pi_entry/runner_live.ts +587 -0
- wmo/harness/pi_entry/runner_service.ts +270 -0
- wmo/harness/pi_entry/runner_stdio.ts +374 -0
- wmo/harness/pi_entry/runner_termination.ts +142 -0
- wmo/harness/pi_local.py +262 -0
- wmo/harness/pi_runtime.py +495 -0
- wmo/harness/pi_vendor.py +65 -0
- wmo/harness/population.py +509 -0
- wmo/harness/project_proposer.py +569 -0
- wmo/harness/proposer.py +977 -0
- wmo/harness/runner_link.py +619 -0
- wmo/harness/runtime.py +389 -0
- wmo/harness/scoring.py +247 -0
- wmo/harness/skills.py +116 -0
- wmo/harness/source_tree.py +319 -0
- wmo/harness/store.py +176 -0
- wmo/harness/tools.py +105 -0
- wmo/harness/vendor/manifest.sha256 +58 -0
- wmo/harness/vendor/pi-agent/CHANGELOG.md +556 -0
- wmo/harness/vendor/pi-agent/LICENSE +21 -0
- wmo/harness/vendor/pi-agent/README.md +488 -0
- wmo/harness/vendor/pi-agent/VENDOR.md +39 -0
- wmo/harness/vendor/pi-agent/docs/agent-harness.md +486 -0
- wmo/harness/vendor/pi-agent/docs/durable-harness.md +212 -0
- wmo/harness/vendor/pi-agent/docs/hooks.md +445 -0
- wmo/harness/vendor/pi-agent/docs/models.md +966 -0
- wmo/harness/vendor/pi-agent/docs/observability.md +376 -0
- wmo/harness/vendor/pi-agent/package.json +60 -0
- wmo/harness/vendor/pi-agent/src/agent-loop.ts +748 -0
- wmo/harness/vendor/pi-agent/src/agent.ts +575 -0
- wmo/harness/vendor/pi-agent/src/harness/agent-harness.ts +1029 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/branch-summarization.ts +261 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/compaction.ts +747 -0
- wmo/harness/vendor/pi-agent/src/harness/compaction/utils.ts +144 -0
- wmo/harness/vendor/pi-agent/src/harness/env/nodejs.ts +550 -0
- wmo/harness/vendor/pi-agent/src/harness/messages.ts +164 -0
- wmo/harness/vendor/pi-agent/src/harness/prompt-templates.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-repo.ts +177 -0
- wmo/harness/vendor/pi-agent/src/harness/session/jsonl-storage.ts +293 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-repo.ts +50 -0
- wmo/harness/vendor/pi-agent/src/harness/session/memory-storage.ts +131 -0
- wmo/harness/vendor/pi-agent/src/harness/session/repo-utils.ts +51 -0
- wmo/harness/vendor/pi-agent/src/harness/session/session.ts +267 -0
- wmo/harness/vendor/pi-agent/src/harness/session/uuid.ts +54 -0
- wmo/harness/vendor/pi-agent/src/harness/skills.ts +375 -0
- wmo/harness/vendor/pi-agent/src/harness/system-prompt.ts +34 -0
- wmo/harness/vendor/pi-agent/src/harness/types.ts +836 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/shell-output.ts +135 -0
- wmo/harness/vendor/pi-agent/src/harness/utils/truncate.ts +344 -0
- wmo/harness/vendor/pi-agent/src/index.ts +44 -0
- wmo/harness/vendor/pi-agent/src/node.ts +2 -0
- wmo/harness/vendor/pi-agent/src/proxy.ts +367 -0
- wmo/harness/vendor/pi-agent/src/types.ts +428 -0
- wmo/harness/vendor/pi-agent/test/agent-loop.test.ts +1351 -0
- wmo/harness/vendor/pi-agent/test/agent.test.ts +699 -0
- wmo/harness/vendor/pi-agent/test/e2e.test.ts +404 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness-stream.test.ts +213 -0
- wmo/harness/vendor/pi-agent/test/harness/agent-harness.test.ts +608 -0
- wmo/harness/vendor/pi-agent/test/harness/compaction.test.ts +655 -0
- wmo/harness/vendor/pi-agent/test/harness/nodejs-env.test.ts +321 -0
- wmo/harness/vendor/pi-agent/test/harness/prompt-templates.test.ts +90 -0
- wmo/harness/vendor/pi-agent/test/harness/repo.test.ts +68 -0
- wmo/harness/vendor/pi-agent/test/harness/resource-formatting.test.ts +24 -0
- wmo/harness/vendor/pi-agent/test/harness/session-test-utils.ts +55 -0
- wmo/harness/vendor/pi-agent/test/harness/session-uuid.test.ts +50 -0
- wmo/harness/vendor/pi-agent/test/harness/session.test.ts +156 -0
- wmo/harness/vendor/pi-agent/test/harness/skills.test.ts +116 -0
- wmo/harness/vendor/pi-agent/test/harness/storage.test.ts +299 -0
- wmo/harness/vendor/pi-agent/test/harness/system-prompt.test.ts +66 -0
- wmo/harness/vendor/pi-agent/test/harness/truncate.test.ts +169 -0
- wmo/harness/vendor/pi-agent/test/scratch/simple.ts +72 -0
- wmo/harness/vendor/pi-agent/test/utils/calculate.ts +32 -0
- wmo/harness/vendor/pi-agent/test/utils/get-current-time.ts +46 -0
- wmo/harness/vendor/pi-agent/tsconfig.build.json +13 -0
- wmo/harness/vendor/pi-agent/vitest.config.ts +19 -0
- wmo/harness/vendor/pi-agent/vitest.harness.config.ts +28 -0
- wmo/harness/vendor/vendor_pi.sh +59 -0
- wmo/harness/workspace_patch.py +270 -0
- wmo/ingest/__init__.py +47 -0
- wmo/ingest/adapter.py +72 -0
- wmo/ingest/base.py +114 -0
- wmo/ingest/braintrust.py +339 -0
- wmo/ingest/detect.py +126 -0
- wmo/ingest/langfuse.py +291 -0
- wmo/ingest/langsmith.py +444 -0
- wmo/ingest/mastra.py +330 -0
- wmo/ingest/messages.py +170 -0
- wmo/ingest/normalize.py +679 -0
- wmo/ingest/otel_genai.py +69 -0
- wmo/ingest/otel_writer.py +100 -0
- wmo/ingest/phoenix.py +150 -0
- wmo/ingest/postgres.py +246 -0
- wmo/ingest/posthog.py +320 -0
- wmo/ingest/quality.py +28 -0
- wmo/ingest/stream.py +209 -0
- wmo/ingest/testdata/sample_otlp.json +60 -0
- wmo/ingest/testdata/sample_spans.jsonl +3 -0
- wmo/optimize/__init__.py +25 -0
- wmo/optimize/base.py +143 -0
- wmo/optimize/gepa.py +806 -0
- wmo/optimize/judge.py +262 -0
- wmo/optimize/judge_quality.py +359 -0
- wmo/optimize/knn.py +468 -0
- wmo/optimize/numeric.py +152 -0
- wmo/optimize/outcomes.py +103 -0
- wmo/optimize/policy.py +669 -0
- wmo/optimize/report.py +231 -0
- wmo/optimize/reward.py +129 -0
- wmo/optimize/routing.py +373 -0
- wmo/platform/__init__.py +6 -0
- wmo/platform/auth.py +115 -0
- wmo/platform/client.py +551 -0
- wmo/platform/credentials.py +126 -0
- wmo/platform/transfer.py +158 -0
- wmo/providers/__init__.py +40 -0
- wmo/providers/_bedrock_chat.py +155 -0
- wmo/providers/_openai_common.py +182 -0
- wmo/providers/_responses_common.py +472 -0
- wmo/providers/anthropic.py +134 -0
- wmo/providers/azure_openai.py +296 -0
- wmo/providers/base.py +300 -0
- wmo/providers/bedrock.py +312 -0
- wmo/providers/models.py +205 -0
- wmo/providers/openai.py +143 -0
- wmo/providers/openai_responses.py +240 -0
- wmo/providers/pool.py +170 -0
- wmo/providers/registry.py +73 -0
- wmo/providers/retry.py +151 -0
- wmo/providers/tinker.py +936 -0
- wmo/providers/waterfall.py +336 -0
- wmo/research/__init__.py +81 -0
- wmo/research/ablation.py +133 -0
- wmo/research/concurrency_plot.py +523 -0
- wmo/research/concurrency_run.py +240 -0
- wmo/research/concurrency_scaling.py +270 -0
- wmo/research/gepa_scaling.py +274 -0
- wmo/research/pipeline.py +198 -0
- wmo/research/scaling_split.py +82 -0
- wmo/research/scenario_fidelity.py +198 -0
- wmo/research/scenario_recovery.py +92 -0
- wmo/research/seed_stability.py +90 -0
- wmo/research/trace_scaling.py +348 -0
- wmo/retrieval/__init__.py +6 -0
- wmo/retrieval/embedders.py +105 -0
- wmo/retrieval/leakfree.py +52 -0
- wmo/retrieval/retriever.py +173 -0
- wmo/scenarios/__init__.py +58 -0
- wmo/scenarios/builder.py +152 -0
- wmo/scenarios/mining/__init__.py +27 -0
- wmo/scenarios/mining/clustering.py +171 -0
- wmo/scenarios/mining/facets.py +226 -0
- wmo/scenarios/mining/selection.py +220 -0
- wmo/scenarios/synthesis/__init__.py +6 -0
- wmo/scenarios/synthesis/scenario_set.py +63 -0
- wmo/scenarios/synthesis/synthesizer.py +85 -0
- wmo/scenarios/verification/__init__.py +17 -0
- wmo/scenarios/verification/judge.py +97 -0
- wmo/scenarios/verification/verify.py +135 -0
- wmo/serving/__init__.py +5 -0
- wmo/serving/builds.py +451 -0
- wmo/serving/chat.py +878 -0
- wmo/serving/endpoint_config.py +64 -0
- wmo/serving/savings.py +250 -0
- wmo/serving/server.py +553 -0
- wmo/serving/traces_source.py +206 -0
- wmo/telemetry.py +213 -0
- wmo/tracking/__init__.py +36 -0
- wmo/tracking/clock.py +24 -0
- wmo/tracking/metered.py +125 -0
- wmo/tracking/pricing.py +99 -0
- wmo/tracking/store.py +31 -0
- wmo/tracking/tracker.py +149 -0
- world_model_optimizer-0.2.0.dist-info/METADATA +203 -0
- world_model_optimizer-0.2.0.dist-info/RECORD +308 -0
- world_model_optimizer-0.2.0.dist-info/WHEEL +4 -0
- world_model_optimizer-0.2.0.dist-info/entry_points.txt +2 -0
wmo/distill/tokens.py
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
"""Token-span records tying harbor trials to the student tokens they sampled.
|
|
2
|
+
|
|
3
|
+
Two span sources feed the same `TrialRecord` shape, one per rollout agent:
|
|
4
|
+
|
|
5
|
+
- **harbor's own terminus-2** (`load_trial_rollout_spans`, the distillation
|
|
6
|
+
path): the agent runs `harbor.llms.tinker.TinkerLLM` with
|
|
7
|
+
`collect_rollout_details=True`, harbor persists the per-turn
|
|
8
|
+
`prompt_token_ids` / `completion_token_ids` / `logprobs` into the trial's
|
|
9
|
+
`result.json` under `agent_result.rollout_details`, and this module reads
|
|
10
|
+
them straight back out. Stop reasons come from
|
|
11
|
+
`read_terminus_stop_reason`, which reconstructs them from the trial's
|
|
12
|
+
recorded exception, its episode count, and the ATIF trajectory's final
|
|
13
|
+
`mark_task_complete`.
|
|
14
|
+
- **the WMO pi bridge** (`load_trial_spans`, the harness-optimization path):
|
|
15
|
+
the distill agent's `TokenRecorder` appends every sampled `TokenSpan` to a
|
|
16
|
+
per-trial JSONL sink named after the harbor trial
|
|
17
|
+
(`{sink_dir}/{trial_name}.jsonl`, one span JSON per line), and the WMO run
|
|
18
|
+
trace (`wmo-run.json`, written into harbor's per-trial agent logs dir)
|
|
19
|
+
supplies the stop reason.
|
|
20
|
+
|
|
21
|
+
Either way `assemble_trial_records` joins the spans with the scorer's reward
|
|
22
|
+
cells into `TrialRecord`s, the unit the datum builder consumes.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import logging
|
|
29
|
+
from collections.abc import Callable, Sequence
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
33
|
+
|
|
34
|
+
from wmo.core.types import JsonObject
|
|
35
|
+
from wmo.harness.runtime import StopReason
|
|
36
|
+
from wmo.harness.scoring import GradedTests, ScoreCell
|
|
37
|
+
from wmo.providers.tinker import TokenSpan
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
WMO_RUN_TRACE_FILENAME = "wmo-run.json"
|
|
42
|
+
|
|
43
|
+
HARBOR_TRIAL_RESULT_FILENAME = "result.json"
|
|
44
|
+
"""Harbor's per-trial `TrialResult` dump (`harbor.models.trial.paths.TrialPaths.result_path`)."""
|
|
45
|
+
|
|
46
|
+
HARBOR_TRAJECTORY_FILENAME = "trajectory.json"
|
|
47
|
+
"""Terminus-2's ATIF trajectory, dumped into its logs dir (`{trial_dir}/agent/`)."""
|
|
48
|
+
|
|
49
|
+
TERMINUS_COMPLETE_TOOL = "mark_task_complete"
|
|
50
|
+
"""The ATIF tool call terminus-2 records when it declares the task finished."""
|
|
51
|
+
|
|
52
|
+
StopReasonReader = Callable[[Path], str | None]
|
|
53
|
+
"""Reads one trial's stop reason from its artifact dir; None when unknown."""
|
|
54
|
+
|
|
55
|
+
SpanLoader = Callable[[Path], list[TokenSpan]]
|
|
56
|
+
"""Reads one trial's sampled token spans from its artifact dir."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TrialRecord(BaseModel):
|
|
60
|
+
"""One harbor trial joined with the exact token spans it sampled.
|
|
61
|
+
|
|
62
|
+
`spans` is the tokens-in-tokens-out training evidence (empty when the
|
|
63
|
+
trial died before its first successful student completion); `reward` and
|
|
64
|
+
`passed` come from the harbor verifier via the scorer's cells and are
|
|
65
|
+
metrics/gating signal, never part of the distillation loss.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
model_config = ConfigDict(extra="forbid")
|
|
69
|
+
|
|
70
|
+
task_id: str = Field(min_length=1)
|
|
71
|
+
attempt: int = Field(ge=1)
|
|
72
|
+
trial_name: str = Field(min_length=1)
|
|
73
|
+
reward: float = Field(ge=0.0, le=1.0, allow_inf_nan=False)
|
|
74
|
+
passed: bool
|
|
75
|
+
spans: list[TokenSpan] = Field(default_factory=list)
|
|
76
|
+
stop_reason: str | None = None
|
|
77
|
+
"""The WMO run trace's stop reason; None when no trace was readable."""
|
|
78
|
+
|
|
79
|
+
infra_failed: bool = False
|
|
80
|
+
"""True when the trial never produced verifier evidence.
|
|
81
|
+
|
|
82
|
+
Two causes, one measurement status: the agent never ran (sandbox/transport death) or its work
|
|
83
|
+
was never graded (the verifier timed out or wrote nothing parseable). Carried from the scorer's
|
|
84
|
+
cell. Its `reward` is a stand-in, so metrics must exclude it from solve-rate denominators and
|
|
85
|
+
count it separately (see `rollout_stats`)."""
|
|
86
|
+
|
|
87
|
+
tests: GradedTests | None = None
|
|
88
|
+
"""The verifier's per-test counts, carried from the scorer's cell; None when it wrote no
|
|
89
|
+
readable report.
|
|
90
|
+
|
|
91
|
+
`graded_score` is the pass fraction over resolved tests: the same trials at test resolution,
|
|
92
|
+
beside (never instead of) the binary `passed`. None is not a 0.0, so `rollout_stats` averages
|
|
93
|
+
only the trials that have one. Absent on records written before this field existed."""
|
|
94
|
+
|
|
95
|
+
artifact_dir: str
|
|
96
|
+
"""The harbor trial directory holding this trial's raw evidence."""
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def graded_score(self) -> float | None:
|
|
100
|
+
"""This trial's graded test-pass score in [0, 1]; None when no test report exists."""
|
|
101
|
+
return None if self.tests is None else self.tests.score
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def load_trial_spans(sink_dir: Path, trial_name: str) -> list[TokenSpan]:
|
|
105
|
+
"""Read the token spans one trial's recorder sink captured.
|
|
106
|
+
|
|
107
|
+
The sink is the JSONL file the distill agent's `TokenRecorder` writes:
|
|
108
|
+
`{trial_name}.jsonl` under `sink_dir`, one `TokenSpan` JSON per line,
|
|
109
|
+
flushed after every successful completion.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
sink_dir: The per-trial token sink directory for one rollout batch.
|
|
113
|
+
trial_name: The harbor trial name the sink file is keyed by.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The recorded spans in call order. A missing sink file yields an empty
|
|
117
|
+
list: the trial made no successful student completion (it died before
|
|
118
|
+
the first sample), which callers count in their stats rather than
|
|
119
|
+
raise on.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ValueError: If a line is not a valid `TokenSpan`, or the call_index
|
|
123
|
+
sequence is not exactly 0..n-1 (the sink was appended by more than
|
|
124
|
+
one recorder); delete the step's token sink directory and re-run
|
|
125
|
+
the step to rebuild it.
|
|
126
|
+
"""
|
|
127
|
+
sink_path = sink_dir / f"{trial_name}.jsonl"
|
|
128
|
+
try:
|
|
129
|
+
text = sink_path.read_text(encoding="utf-8")
|
|
130
|
+
except FileNotFoundError:
|
|
131
|
+
return []
|
|
132
|
+
spans: list[TokenSpan] = []
|
|
133
|
+
for line_number, line in enumerate(text.splitlines(), 1):
|
|
134
|
+
if not line.strip():
|
|
135
|
+
continue
|
|
136
|
+
try:
|
|
137
|
+
spans.append(TokenSpan.model_validate_json(line))
|
|
138
|
+
except ValidationError as exc:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"invalid token span on line {line_number} of {sink_path}: {exc}; "
|
|
141
|
+
"the sink is corrupt, so delete this step's token sink directory "
|
|
142
|
+
"and re-run the step"
|
|
143
|
+
) from exc
|
|
144
|
+
observed = [span.call_index for span in spans]
|
|
145
|
+
if observed != list(range(len(spans))):
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"token sink {sink_path} has call_index sequence {observed}, expected "
|
|
148
|
+
f"0..{len(spans) - 1}; more than one recorder appended to it (e.g. a "
|
|
149
|
+
"re-run trial reused the sink), so delete this step's token sink "
|
|
150
|
+
"directory and re-run the step"
|
|
151
|
+
)
|
|
152
|
+
return spans
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _read_json_object(path: Path) -> JsonObject | None:
|
|
156
|
+
"""One JSON object from disk, or None when it is missing or unreadable.
|
|
157
|
+
|
|
158
|
+
Never raises: assembly must survive the trials that died hardest, where a
|
|
159
|
+
truncated or absent artifact IS the evidence.
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
163
|
+
except FileNotFoundError:
|
|
164
|
+
return None
|
|
165
|
+
except (OSError, json.JSONDecodeError):
|
|
166
|
+
logger.warning("unreadable harbor artifact at %s", path)
|
|
167
|
+
return None
|
|
168
|
+
return payload if isinstance(payload, dict) else None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _int_lists(value: object) -> list[list[int]] | None:
|
|
172
|
+
"""`value` as a list of int lists, or None when it is not shaped like one."""
|
|
173
|
+
if not isinstance(value, list):
|
|
174
|
+
return None
|
|
175
|
+
rows: list[list[int]] = []
|
|
176
|
+
for row in value:
|
|
177
|
+
if not isinstance(row, list):
|
|
178
|
+
return None
|
|
179
|
+
ints: list[int] = []
|
|
180
|
+
for item in row:
|
|
181
|
+
if isinstance(item, bool) or not isinstance(item, int):
|
|
182
|
+
return None
|
|
183
|
+
ints.append(item)
|
|
184
|
+
rows.append(ints)
|
|
185
|
+
return rows
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _float_lists(value: object) -> list[list[float]] | None:
|
|
189
|
+
"""`value` as a list of float lists, or None when it is not shaped like one."""
|
|
190
|
+
if not isinstance(value, list):
|
|
191
|
+
return None
|
|
192
|
+
rows: list[list[float]] = []
|
|
193
|
+
for row in value:
|
|
194
|
+
if not isinstance(row, list):
|
|
195
|
+
return None
|
|
196
|
+
floats: list[float] = []
|
|
197
|
+
for item in row:
|
|
198
|
+
if isinstance(item, bool) or not isinstance(item, (int, float)):
|
|
199
|
+
return None
|
|
200
|
+
floats.append(float(item))
|
|
201
|
+
rows.append(floats)
|
|
202
|
+
return rows
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def load_trial_rollout_spans(artifact_dir: Path) -> list[TokenSpan]:
|
|
206
|
+
"""Read the token spans harbor recorded for one terminus-2 trial.
|
|
207
|
+
|
|
208
|
+
The source is harbor's own persisted evidence, not a WMO sink: the trial's
|
|
209
|
+
`result.json` carries `agent_result.rollout_details`, a list of
|
|
210
|
+
`harbor.models.agent.rollout_detail.RolloutDetail`, and by harbor's
|
|
211
|
+
convention its FIRST entry is the main agent's linear chat history. Each
|
|
212
|
+
entry holds three per-turn lists (`prompt_token_ids`,
|
|
213
|
+
`completion_token_ids`, `logprobs`) that terminus-2's `TinkerLLM`
|
|
214
|
+
populates with the exact ids the sampler consumed and issued, so the
|
|
215
|
+
tokens-in-tokens-out contract is preserved verbatim through the file.
|
|
216
|
+
|
|
217
|
+
Later entries are SUBAGENT segments (terminus-2 appends one per
|
|
218
|
+
summarization handoff); they are separate conversations whose prompts do
|
|
219
|
+
not extend the main chat, so they are skipped and warned about. The
|
|
220
|
+
distill collector disables summarization precisely so none exist.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
artifact_dir: The harbor trial directory (one cell's `artifact_dir`).
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
The spans in turn order, `call_index` 0..n-1. Empty when the trial
|
|
227
|
+
never reached a completion, when harbor recorded no rollout details
|
|
228
|
+
(the agent ran without `collect_rollout_details`, or died before its
|
|
229
|
+
first sample), or when the recorded lists cannot be aligned. Every
|
|
230
|
+
non-trivial empty case is logged at WARNING and surfaces in
|
|
231
|
+
`RolloutStats.empty_span_trials`: a trial that produced no training
|
|
232
|
+
evidence must be counted, never silently dropped and never allowed to
|
|
233
|
+
abort the batch around it.
|
|
234
|
+
"""
|
|
235
|
+
payload = _read_json_object(artifact_dir / HARBOR_TRIAL_RESULT_FILENAME)
|
|
236
|
+
if payload is None:
|
|
237
|
+
return []
|
|
238
|
+
agent_result = payload.get("agent_result")
|
|
239
|
+
if not isinstance(agent_result, dict):
|
|
240
|
+
if payload.get("step_results"):
|
|
241
|
+
logger.warning(
|
|
242
|
+
"harbor trial %s is a MULTI-STEP trial: its per-step chats are separate "
|
|
243
|
+
"conversations, so no single prefix-chained episode exists to train on",
|
|
244
|
+
artifact_dir.name,
|
|
245
|
+
)
|
|
246
|
+
return []
|
|
247
|
+
details = agent_result.get("rollout_details")
|
|
248
|
+
if not isinstance(details, list) or not details:
|
|
249
|
+
return []
|
|
250
|
+
if len(details) > 1:
|
|
251
|
+
logger.warning(
|
|
252
|
+
"harbor trial %s recorded %d rollout detail segments; only the first (the main "
|
|
253
|
+
"agent chat) is trainable, the rest are subagent conversations whose prompts do "
|
|
254
|
+
"not extend it. Summarization must stay OFF for distillation rollouts",
|
|
255
|
+
artifact_dir.name,
|
|
256
|
+
len(details),
|
|
257
|
+
)
|
|
258
|
+
detail = details[0]
|
|
259
|
+
if not isinstance(detail, dict):
|
|
260
|
+
logger.warning("harbor trial %s recorded a malformed rollout detail", artifact_dir.name)
|
|
261
|
+
return []
|
|
262
|
+
prompts = _int_lists(detail.get("prompt_token_ids"))
|
|
263
|
+
completions = _int_lists(detail.get("completion_token_ids"))
|
|
264
|
+
logprobs = _float_lists(detail.get("logprobs"))
|
|
265
|
+
if prompts is None or completions is None or logprobs is None:
|
|
266
|
+
logger.warning(
|
|
267
|
+
"harbor trial %s recorded rollout details without well-formed per-turn token id "
|
|
268
|
+
"lists (prompt=%s completion=%s logprobs=%s); it contributes no training data",
|
|
269
|
+
artifact_dir.name,
|
|
270
|
+
type(detail.get("prompt_token_ids")).__name__,
|
|
271
|
+
type(detail.get("completion_token_ids")).__name__,
|
|
272
|
+
type(detail.get("logprobs")).__name__,
|
|
273
|
+
)
|
|
274
|
+
return []
|
|
275
|
+
if not (len(prompts) == len(completions) == len(logprobs)):
|
|
276
|
+
# harbor's Chat appends to the three lists independently, so a turn that returned no
|
|
277
|
+
# completion (or no logprobs) desynchronizes them for every later turn. Nothing can
|
|
278
|
+
# realign them after the fact, and pairing them anyway would train the wrong prompts.
|
|
279
|
+
logger.warning(
|
|
280
|
+
"harbor trial %s recorded %d prompt turn(s), %d completion turn(s) and %d logprob "
|
|
281
|
+
"turn(s); the per-turn lists cannot be aligned, so the trial contributes no "
|
|
282
|
+
"training data",
|
|
283
|
+
artifact_dir.name,
|
|
284
|
+
len(prompts),
|
|
285
|
+
len(completions),
|
|
286
|
+
len(logprobs),
|
|
287
|
+
)
|
|
288
|
+
return []
|
|
289
|
+
spans: list[TokenSpan] = []
|
|
290
|
+
for index, (prompt, completion, row) in enumerate(
|
|
291
|
+
zip(prompts, completions, logprobs, strict=True)
|
|
292
|
+
):
|
|
293
|
+
try:
|
|
294
|
+
spans.append(
|
|
295
|
+
TokenSpan(
|
|
296
|
+
call_index=index,
|
|
297
|
+
prompt_token_ids=prompt,
|
|
298
|
+
sampled_token_ids=completion,
|
|
299
|
+
sampled_logprobs=row,
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
except ValidationError as exc:
|
|
303
|
+
logger.warning(
|
|
304
|
+
"harbor trial %s turn %d records %d sampled token(s) against %d logprob(s) "
|
|
305
|
+
"(%s); the trial contributes no training data",
|
|
306
|
+
artifact_dir.name,
|
|
307
|
+
index,
|
|
308
|
+
len(completion),
|
|
309
|
+
len(row),
|
|
310
|
+
exc,
|
|
311
|
+
)
|
|
312
|
+
return []
|
|
313
|
+
return spans
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
_TERMINUS_EXCEPTION_STOP_REASONS = {
|
|
317
|
+
# Harbor's own agent-phase wall clock (`AgentConfig.override_timeout_sec`, which is where
|
|
318
|
+
# `rollout.episode_timeout_s` lands): harbor swallows it and still verifies the work.
|
|
319
|
+
"AgentTimeoutError": StopReason.BUDGET,
|
|
320
|
+
# Terminus-2 re-raises this when summarization is off, which is how distillation runs it:
|
|
321
|
+
# the next prompt outgrew `rollout.context_budget_tokens` and nothing was sampled from it.
|
|
322
|
+
"ContextLengthExceededError": StopReason.PROVIDER_ERROR,
|
|
323
|
+
}
|
|
324
|
+
"""Terminus-2 trial exceptions that name their own stop reason; anything else is `ERROR`."""
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _terminus_declared_complete(artifact_dir: Path) -> bool:
|
|
328
|
+
"""True when terminus-2's final agent step declared the task complete.
|
|
329
|
+
|
|
330
|
+
Read from the ATIF trajectory terminus-2 dumps after every episode, so it
|
|
331
|
+
survives a trial that died later. `mark_task_complete` is the tool call it
|
|
332
|
+
records for its own completion claim, which is the closest analogue of
|
|
333
|
+
pi's explicit `submit`.
|
|
334
|
+
"""
|
|
335
|
+
payload = _read_json_object(artifact_dir / "agent" / HARBOR_TRAJECTORY_FILENAME)
|
|
336
|
+
steps = payload.get("steps") if payload is not None else None
|
|
337
|
+
if not isinstance(steps, list):
|
|
338
|
+
return False
|
|
339
|
+
for step in reversed(steps):
|
|
340
|
+
if not isinstance(step, dict) or step.get("source") != "agent":
|
|
341
|
+
continue
|
|
342
|
+
calls = step.get("tool_calls")
|
|
343
|
+
if not isinstance(calls, list):
|
|
344
|
+
return False
|
|
345
|
+
return any(
|
|
346
|
+
isinstance(call, dict) and call.get("function_name") == TERMINUS_COMPLETE_TOOL
|
|
347
|
+
for call in calls
|
|
348
|
+
)
|
|
349
|
+
return False
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def read_terminus_stop_reason(artifact_dir: Path, *, max_turns: int) -> str | None:
|
|
353
|
+
"""Why one terminus-2 episode ended, in WMO's `StopReason` vocabulary.
|
|
354
|
+
|
|
355
|
+
Terminus-2 writes no WMO run trace, so the reason is reconstructed from
|
|
356
|
+
the three artifacts harbor and the agent do leave behind, in the order
|
|
357
|
+
that makes each one decisive:
|
|
358
|
+
|
|
359
|
+
1. the trial's recorded exception (`AgentTimeoutError` is the wall clock,
|
|
360
|
+
`ContextLengthExceededError` is the context budget, anything else is a
|
|
361
|
+
harness/runtime error);
|
|
362
|
+
2. the episode count terminus-2 records in
|
|
363
|
+
`agent_result.metadata.n_episodes`: reaching `max_turns` means the loop
|
|
364
|
+
was cut off by the cap;
|
|
365
|
+
3. the ATIF trajectory's final `mark_task_complete`, terminus-2's own
|
|
366
|
+
completion claim.
|
|
367
|
+
|
|
368
|
+
Deliberately conservative at the boundary: an episode that declared
|
|
369
|
+
completion on exactly its last allowed turn reads as `max_turns`, i.e. as
|
|
370
|
+
a scaffold loss, rather than as a submission the cap happened to permit.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
artifact_dir: The harbor trial directory (one cell's `artifact_dir`).
|
|
374
|
+
max_turns: The turn cap the agent ran under (`rollout.max_turns`).
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
A `StopReason` value, or None when no trial result was readable at all
|
|
378
|
+
(which `rollout_stats` counts as `"unknown"` and excludes from the
|
|
379
|
+
scaffold-loss denominator rather than guessing).
|
|
380
|
+
"""
|
|
381
|
+
payload = _read_json_object(artifact_dir / HARBOR_TRIAL_RESULT_FILENAME)
|
|
382
|
+
if payload is None:
|
|
383
|
+
return None
|
|
384
|
+
exception = payload.get("exception_info")
|
|
385
|
+
if isinstance(exception, dict):
|
|
386
|
+
raised = exception.get("exception_type")
|
|
387
|
+
if isinstance(raised, str):
|
|
388
|
+
return _TERMINUS_EXCEPTION_STOP_REASONS.get(raised, StopReason.ERROR).value
|
|
389
|
+
agent_result = payload.get("agent_result")
|
|
390
|
+
metadata = agent_result.get("metadata") if isinstance(agent_result, dict) else None
|
|
391
|
+
episodes = metadata.get("n_episodes") if isinstance(metadata, dict) else None
|
|
392
|
+
if isinstance(episodes, bool) or not isinstance(episodes, int):
|
|
393
|
+
return None
|
|
394
|
+
if episodes >= max_turns:
|
|
395
|
+
return StopReason.MAX_TURNS.value
|
|
396
|
+
if _terminus_declared_complete(artifact_dir):
|
|
397
|
+
return StopReason.SUBMITTED.value
|
|
398
|
+
# The loop returned early without claiming completion: terminus-2 does that only when its
|
|
399
|
+
# tmux session died under it, which is a harness failure, not a task verdict.
|
|
400
|
+
return StopReason.ERROR.value
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def read_trial_stop_reason(artifact_dir: Path) -> str | None:
|
|
404
|
+
"""The WMO run trace's stop reason for one trial, when a trace exists.
|
|
405
|
+
|
|
406
|
+
The agent bridge writes `wmo-run.json` into harbor's per-trial agent logs
|
|
407
|
+
dir (`{trial_dir}/agent/` for single-step tasks); both full `RunResult`
|
|
408
|
+
dumps and partial cancellation traces carry a string `stop_reason`. The
|
|
409
|
+
trial-dir root is also checked for robustness against layout drift.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
artifact_dir: The harbor trial directory (one cell's `artifact_dir`).
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
The stop reason string, or None when no trace is present or readable.
|
|
416
|
+
Assembly must not fail on the trials that died hardest; a missing
|
|
417
|
+
stop reason is itself the signal.
|
|
418
|
+
"""
|
|
419
|
+
candidates = (
|
|
420
|
+
artifact_dir / "agent" / WMO_RUN_TRACE_FILENAME,
|
|
421
|
+
artifact_dir / WMO_RUN_TRACE_FILENAME,
|
|
422
|
+
)
|
|
423
|
+
for candidate in candidates:
|
|
424
|
+
if not candidate.is_file():
|
|
425
|
+
continue
|
|
426
|
+
try:
|
|
427
|
+
payload = json.loads(candidate.read_text(encoding="utf-8"))
|
|
428
|
+
except (OSError, json.JSONDecodeError):
|
|
429
|
+
logger.warning("unreadable WMO run trace at %s; recording no stop reason", candidate)
|
|
430
|
+
return None
|
|
431
|
+
stop_reason = payload.get("stop_reason") if isinstance(payload, dict) else None
|
|
432
|
+
return stop_reason if isinstance(stop_reason, str) else None
|
|
433
|
+
return None
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def assemble_harbor_trial_records(
|
|
437
|
+
cells: Sequence[ScoreCell], *, max_turns: int
|
|
438
|
+
) -> list[TrialRecord]:
|
|
439
|
+
"""Join scorer cells with the spans HARBOR recorded for its terminus-2 agent.
|
|
440
|
+
|
|
441
|
+
The distillation path's assembler: spans come from each trial's
|
|
442
|
+
`result.json` (`load_trial_rollout_spans`) and stop reasons are
|
|
443
|
+
reconstructed from harbor's own artifacts (`read_terminus_stop_reason`).
|
|
444
|
+
No WMO-side sink directory is involved, so nothing has to survive the
|
|
445
|
+
scorer's destructive entry prune.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
cells: The scored trial cells (one per task x attempt).
|
|
449
|
+
max_turns: The turn cap the agent ran under, needed to tell a
|
|
450
|
+
cap-terminated episode from a completed one.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
One `TrialRecord` per cell, in the cells' order.
|
|
454
|
+
|
|
455
|
+
Raises:
|
|
456
|
+
ValueError: If a cell carries no artifact dir.
|
|
457
|
+
"""
|
|
458
|
+
return _assemble_trial_records(
|
|
459
|
+
cells,
|
|
460
|
+
load_spans=load_trial_rollout_spans,
|
|
461
|
+
read_stop_reason=lambda artifact_dir: read_terminus_stop_reason(
|
|
462
|
+
artifact_dir, max_turns=max_turns
|
|
463
|
+
),
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def assemble_trial_records(
|
|
468
|
+
cells: Sequence[ScoreCell],
|
|
469
|
+
sink_dir: Path,
|
|
470
|
+
*,
|
|
471
|
+
read_stop_reason: StopReasonReader | None = None,
|
|
472
|
+
) -> list[TrialRecord]:
|
|
473
|
+
"""Join scorer cells with their token sinks and run traces.
|
|
474
|
+
|
|
475
|
+
The WMO pi-bridge path's assembler (see `assemble_harbor_trial_records`
|
|
476
|
+
for the terminus-2 one).
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
cells: The scored trial cells (one per task x attempt); each cell's
|
|
480
|
+
`artifact_dir` must be the harbor trial directory, whose basename
|
|
481
|
+
is the trial name the token sinks are keyed by.
|
|
482
|
+
sink_dir: The per-trial token sink directory for this rollout batch.
|
|
483
|
+
read_stop_reason: Reads one trial's stop reason from its artifact dir;
|
|
484
|
+
defaults to `read_trial_stop_reason`.
|
|
485
|
+
|
|
486
|
+
Returns:
|
|
487
|
+
One `TrialRecord` per cell, in the cells' order. A trial without a
|
|
488
|
+
sink file gets empty spans, never dropped: its reward is still real
|
|
489
|
+
batch signal and callers count span-less trials explicitly.
|
|
490
|
+
|
|
491
|
+
Raises:
|
|
492
|
+
ValueError: If a cell carries no artifact dir (the trial name cannot
|
|
493
|
+
be derived), or a sink file is corrupt (see `load_trial_spans`).
|
|
494
|
+
"""
|
|
495
|
+
return _assemble_trial_records(
|
|
496
|
+
cells,
|
|
497
|
+
load_spans=lambda artifact_dir: load_trial_spans(sink_dir, artifact_dir.name),
|
|
498
|
+
read_stop_reason=read_stop_reason or read_trial_stop_reason,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _assemble_trial_records(
|
|
503
|
+
cells: Sequence[ScoreCell],
|
|
504
|
+
*,
|
|
505
|
+
load_spans: SpanLoader,
|
|
506
|
+
read_stop_reason: StopReasonReader,
|
|
507
|
+
) -> list[TrialRecord]:
|
|
508
|
+
"""Join cells with whichever span source and stop-reason reader the caller uses."""
|
|
509
|
+
reader = read_stop_reason
|
|
510
|
+
records: list[TrialRecord] = []
|
|
511
|
+
for cell in cells:
|
|
512
|
+
artifact_dir = Path(cell.artifact_dir)
|
|
513
|
+
trial_name = artifact_dir.name
|
|
514
|
+
if not cell.artifact_dir or not trial_name:
|
|
515
|
+
raise ValueError(
|
|
516
|
+
f"score cell for task {cell.task_id!r} attempt {cell.attempt} carries no "
|
|
517
|
+
"artifact dir, so its trial name (the token-sink key) cannot be derived; "
|
|
518
|
+
"collect rollouts through a scorer that records per-trial directories "
|
|
519
|
+
"(HarborScorer does)"
|
|
520
|
+
)
|
|
521
|
+
records.append(
|
|
522
|
+
TrialRecord(
|
|
523
|
+
task_id=cell.task_id,
|
|
524
|
+
attempt=cell.attempt,
|
|
525
|
+
trial_name=trial_name,
|
|
526
|
+
reward=cell.reward,
|
|
527
|
+
passed=cell.passed,
|
|
528
|
+
spans=load_spans(artifact_dir),
|
|
529
|
+
stop_reason=reader(artifact_dir),
|
|
530
|
+
infra_failed=cell.infra_failed,
|
|
531
|
+
tests=cell.tests,
|
|
532
|
+
artifact_dir=cell.artifact_dir,
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
return records
|