vis-agent 0.2.38.dev7557__tar.gz → 0.2.38.dev7568__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/.gitignore +3 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/PKG-INFO +1 -1
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/pyproject.toml +1 -1
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/activity.py +1 -0
- vis_agent-0.2.38.dev7568/src/blockether/vis/evals/__init__.py +68 -0
- vis_agent-0.2.38.dev7568/src/blockether/vis/evals/harbor.py +303 -0
- vis_agent-0.2.38.dev7568/src/blockether/vis/evals/stats.py +170 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/evals/trajectory.py +29 -8
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_activity.py +16 -0
- vis_agent-0.2.38.dev7568/tests/test_evals_harbor.py +228 -0
- vis_agent-0.2.38.dev7568/tests/test_evals_stats.py +94 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_evals_trajectory.py +19 -2
- vis_agent-0.2.38.dev7557/src/blockether/vis/evals/__init__.py +0 -26
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/LICENSE +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/README.md +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/fixtures/activity-arguments.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/fixtures/activity-cases.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/fixtures/activity-copy.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/fixtures/activity-execution.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/fixtures/activity-groups.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/fixtures/activity-read-session.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/fixtures/activity-reads.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/fixtures/activity-results.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/fixtures/activity-tables.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/fixtures/activity.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/fixtures/diff.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/fixtures/live-primitives.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/fixtures/session-health.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/fixtures/view.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/activity.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/agents.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/automations.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/common.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/config.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/content.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/council.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/diff.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/gateway.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/improve.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/provider.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/rooms.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/symbol.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/toggle.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/contracts/schema/view.json +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/examples/greeter/tests/test_greeter.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/hatch_build.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/__init__.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/_contracts.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/_outside.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/_wire.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/engine/__init__.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/engine/_agent.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/engine/_agents.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/engine/_client.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/engine/_council.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/engine/_extensions.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/engine/_local.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/extension.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/extension_package.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/py.typed +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/rooms.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/views.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/conftest.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_agent.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_agent_extension_lifecycle.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_agent_extensions.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_agents.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_api_guides.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_authoring.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_catalog.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_catalog_recipe.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_ci_report_recipe.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_client.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_client_extensions_engine.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_code_quality_recipe.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_contracts.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_council.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_declarations.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_engine.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_engine_docstrings.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_extension_package.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_extension_reference.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_gateway_performance.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_goals.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_live_primitives.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_live_wait.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_local.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_method_typing.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_monitor_recipe.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_namespace.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_outside.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_providers.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_record_arguments.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_registration.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_repair_hooks.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_result_sequence.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_rooms.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_scoped_settings.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_sdk_bootstrap.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_sdk_guide.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_sdk_typing.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_structured_live.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_symbol_contract.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_testing.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_text_list_arguments.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_tools.py +0 -0
- {vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/tests/test_views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vis-agent
|
|
3
|
-
Version: 0.2.38.
|
|
3
|
+
Version: 0.2.38.dev7568
|
|
4
4
|
Summary: The Vis Python SDK — author extensions and control local or remote engines.
|
|
5
5
|
Project-URL: Homepage, https://vis.blockether.com
|
|
6
6
|
Project-URL: Documentation, https://vis.blockether.com
|
|
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "vis-agent"
|
|
11
|
-
version = "0.2.38.
|
|
11
|
+
version = "0.2.38.dev7568"
|
|
12
12
|
description = "The Vis Python SDK — author extensions and control local or remote engines."
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
requires-python = ">=3.11"
|
|
@@ -84,6 +84,7 @@ class ActivityRow:
|
|
|
84
84
|
resources: tuple[ActivityResource, ...]
|
|
85
85
|
evidence: tuple[ActivityEvidence, ...]
|
|
86
86
|
argument_key: str | None = None
|
|
87
|
+
read_key: str | None = None
|
|
87
88
|
handle_id: str | None = None
|
|
88
89
|
group_token: str | None = None
|
|
89
90
|
duration_ms: int | None = None
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Evaluation tools: read Vis runs and Harbor jobs, and compare their results.
|
|
2
|
+
|
|
3
|
+
Read one Vis run with `load_trajectory` or `parse_trajectory`. Read the trials of a
|
|
4
|
+
Harbor job or round with `read_trials`, then count them with `case_results` or
|
|
5
|
+
`summarize`. Seal a finished round with `seal_round`. Compare paired results with
|
|
6
|
+
`paired_change` and `mcnemar_exact`. For installation and sessions, use the
|
|
7
|
+
[Python SDK guide](https://vis.blockether.com/python-sdk.html).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .harbor import (
|
|
11
|
+
HarborTrial,
|
|
12
|
+
RunSummary,
|
|
13
|
+
case_results,
|
|
14
|
+
read_trial,
|
|
15
|
+
read_trials,
|
|
16
|
+
seal_digest,
|
|
17
|
+
seal_round,
|
|
18
|
+
seal_rows,
|
|
19
|
+
summarize,
|
|
20
|
+
)
|
|
21
|
+
from .stats import (
|
|
22
|
+
benjamini_hochberg,
|
|
23
|
+
cluster_interval,
|
|
24
|
+
cohen_kappa,
|
|
25
|
+
detectable_change,
|
|
26
|
+
effective_size,
|
|
27
|
+
mcnemar_exact,
|
|
28
|
+
paired_change,
|
|
29
|
+
wilson_interval,
|
|
30
|
+
)
|
|
31
|
+
from .trajectory import (
|
|
32
|
+
FunctionCall,
|
|
33
|
+
Step,
|
|
34
|
+
StepMetrics,
|
|
35
|
+
ToolCall,
|
|
36
|
+
Trajectory,
|
|
37
|
+
TrajectoryMetrics,
|
|
38
|
+
load_trajectory,
|
|
39
|
+
parse_trajectory,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"FunctionCall",
|
|
44
|
+
"HarborTrial",
|
|
45
|
+
"RunSummary",
|
|
46
|
+
"Step",
|
|
47
|
+
"StepMetrics",
|
|
48
|
+
"ToolCall",
|
|
49
|
+
"Trajectory",
|
|
50
|
+
"TrajectoryMetrics",
|
|
51
|
+
"benjamini_hochberg",
|
|
52
|
+
"case_results",
|
|
53
|
+
"cluster_interval",
|
|
54
|
+
"cohen_kappa",
|
|
55
|
+
"detectable_change",
|
|
56
|
+
"effective_size",
|
|
57
|
+
"load_trajectory",
|
|
58
|
+
"mcnemar_exact",
|
|
59
|
+
"paired_change",
|
|
60
|
+
"parse_trajectory",
|
|
61
|
+
"read_trial",
|
|
62
|
+
"read_trials",
|
|
63
|
+
"seal_digest",
|
|
64
|
+
"seal_round",
|
|
65
|
+
"seal_rows",
|
|
66
|
+
"summarize",
|
|
67
|
+
"wilson_interval",
|
|
68
|
+
]
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""Read Harbor jobs: trials, cases, a summary with the cost, and round seals.
|
|
2
|
+
|
|
3
|
+
A Harbor job directory holds `config.json`, `result.json` and one directory for each
|
|
4
|
+
trial. A trial directory holds `result.json`, `agent/trajectory.json` and
|
|
5
|
+
`verifier/reward.json`. A round directory holds one or more job directories.
|
|
6
|
+
|
|
7
|
+
This module reads the JSON files directly, so it does not need Harbor itself.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import time
|
|
15
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from types import MappingProxyType
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from .stats import cluster_interval
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"HarborTrial",
|
|
25
|
+
"RunSummary",
|
|
26
|
+
"case_results",
|
|
27
|
+
"read_trial",
|
|
28
|
+
"read_trials",
|
|
29
|
+
"seal_digest",
|
|
30
|
+
"seal_round",
|
|
31
|
+
"seal_rows",
|
|
32
|
+
"summarize",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
RESULT_FILE = "result.json"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True, slots=True)
|
|
39
|
+
class HarborTrial:
|
|
40
|
+
"""One Harbor trial, read from its `result.json`.
|
|
41
|
+
|
|
42
|
+
`task` is the Harbor task name. `agent` is the agent name that the trial reports.
|
|
43
|
+
`model` is the model name of the trial configuration, for example
|
|
44
|
+
`openai/gpt-6.1-sol`. `reward` is the `reward` value of the verifier. It is None when
|
|
45
|
+
the verifier gave no reward, for example after an infrastructure error. `rewards`
|
|
46
|
+
holds all named rewards. `error` is the exception type of a failed trial. The token
|
|
47
|
+
counts and `cost_usd` are None when the agent did not report them. `path` is the
|
|
48
|
+
`result.json` file.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
job: str
|
|
52
|
+
trial: str
|
|
53
|
+
task: str
|
|
54
|
+
agent: str
|
|
55
|
+
agent_version: str | None
|
|
56
|
+
model: str
|
|
57
|
+
reward: float | None
|
|
58
|
+
rewards: Mapping[str, float]
|
|
59
|
+
error: str | None
|
|
60
|
+
input_tokens: int | None
|
|
61
|
+
cache_tokens: int | None
|
|
62
|
+
output_tokens: int | None
|
|
63
|
+
cost_usd: float | None
|
|
64
|
+
task_checksum: str | None
|
|
65
|
+
started_at: str | None
|
|
66
|
+
finished_at: str | None
|
|
67
|
+
path: Path
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def is_scored(self) -> bool:
|
|
71
|
+
"""True when the verifier gave a reward."""
|
|
72
|
+
return self.reward is not None
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def is_passed(self) -> bool:
|
|
76
|
+
"""True when the reward is 1 or more."""
|
|
77
|
+
return self.reward is not None and self.reward >= 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True, slots=True)
|
|
81
|
+
class RunSummary:
|
|
82
|
+
"""The counts, the pass rate and the cost of a set of trials.
|
|
83
|
+
|
|
84
|
+
A case is one task with one agent and one model. `pass_rate` is `passed / scored`,
|
|
85
|
+
and `pass_rate_ci95` is its 95% cluster interval over the cases. Both are
|
|
86
|
+
fractions, or None without a scored trial. `errors` counts the trials with an
|
|
87
|
+
exception. `cost_usd` is the sum of the reported costs, or None when no trial
|
|
88
|
+
reports a cost. `cost_per_pass_usd` divides that sum by the passed trials.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
trials: int
|
|
92
|
+
scored: int
|
|
93
|
+
passed: int
|
|
94
|
+
errors: int
|
|
95
|
+
cases: int
|
|
96
|
+
pass_rate: float | None
|
|
97
|
+
pass_rate_ci95: tuple[float, float] | None
|
|
98
|
+
input_tokens: int
|
|
99
|
+
cache_tokens: int
|
|
100
|
+
output_tokens: int
|
|
101
|
+
cost_usd: float | None
|
|
102
|
+
cost_per_pass_usd: float | None
|
|
103
|
+
tasks: tuple[str, ...]
|
|
104
|
+
agents: tuple[str, ...]
|
|
105
|
+
models: tuple[str, ...]
|
|
106
|
+
started_at: str | None
|
|
107
|
+
finished_at: str | None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _integer(value: Any) -> int | None:
|
|
111
|
+
return value if isinstance(value, int) and not isinstance(value, bool) else None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _number(value: Any) -> float | None:
|
|
115
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
116
|
+
return None
|
|
117
|
+
return float(value)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _text(value: Any) -> str | None:
|
|
121
|
+
return str(value) if value not in (None, "") else None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def read_trial(path: Path | str) -> HarborTrial | None:
|
|
125
|
+
"""Read one finished trial from its `result.json`, or return None for other files.
|
|
126
|
+
|
|
127
|
+
A job `result.json`, an unfinished trial and a file that is not JSON give None.
|
|
128
|
+
"""
|
|
129
|
+
path = Path(path)
|
|
130
|
+
try:
|
|
131
|
+
data = json.loads(path.read_text())
|
|
132
|
+
except (OSError, ValueError):
|
|
133
|
+
return None
|
|
134
|
+
if not isinstance(data, dict):
|
|
135
|
+
return None
|
|
136
|
+
if not (
|
|
137
|
+
data.get("task_name") and data.get("trial_name") and data.get("finished_at")
|
|
138
|
+
):
|
|
139
|
+
return None
|
|
140
|
+
agent = (data.get("config") or {}).get("agent") or {}
|
|
141
|
+
info = data.get("agent_info") or {}
|
|
142
|
+
usage = data.get("agent_result") or {}
|
|
143
|
+
rewards = {
|
|
144
|
+
str(name): number
|
|
145
|
+
for name, value in (
|
|
146
|
+
(data.get("verifier_result") or {}).get("rewards") or {}
|
|
147
|
+
).items()
|
|
148
|
+
if (number := _number(value)) is not None
|
|
149
|
+
}
|
|
150
|
+
return HarborTrial(
|
|
151
|
+
job=path.parent.parent.name,
|
|
152
|
+
trial=str(data["trial_name"]),
|
|
153
|
+
task=str(data["task_name"]),
|
|
154
|
+
agent=str(
|
|
155
|
+
info.get("name") or agent.get("name") or agent.get("import_path") or ""
|
|
156
|
+
),
|
|
157
|
+
agent_version=_text(info.get("version")),
|
|
158
|
+
model=str(agent.get("model_name") or ""),
|
|
159
|
+
reward=rewards.get("reward"),
|
|
160
|
+
rewards=MappingProxyType(rewards),
|
|
161
|
+
error=_text((data.get("exception_info") or {}).get("exception_type")),
|
|
162
|
+
input_tokens=_integer(usage.get("n_input_tokens")),
|
|
163
|
+
cache_tokens=_integer(usage.get("n_cache_tokens")),
|
|
164
|
+
output_tokens=_integer(usage.get("n_output_tokens")),
|
|
165
|
+
cost_usd=_number(usage.get("cost_usd")),
|
|
166
|
+
task_checksum=_text(data.get("task_checksum")),
|
|
167
|
+
started_at=_text(data.get("started_at")),
|
|
168
|
+
finished_at=_text(data.get("finished_at")),
|
|
169
|
+
path=path,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def read_trials(directory: Path | str) -> tuple[HarborTrial, ...]:
|
|
174
|
+
"""Read the finished trials of a Harbor job directory or of a round of jobs.
|
|
175
|
+
|
|
176
|
+
A trial `result.json` is one or two levels below `directory`. The trials come in
|
|
177
|
+
start order. A directory that does not exist raises `FileNotFoundError`.
|
|
178
|
+
"""
|
|
179
|
+
root = Path(directory)
|
|
180
|
+
if not root.is_dir():
|
|
181
|
+
raise FileNotFoundError(f"no Harbor job or round directory at {root}")
|
|
182
|
+
paths = sorted({*root.glob(f"*/{RESULT_FILE}"), *root.glob(f"*/*/{RESULT_FILE}")})
|
|
183
|
+
trials = [trial for trial in map(read_trial, paths) if trial is not None]
|
|
184
|
+
return tuple(
|
|
185
|
+
sorted(trials, key=lambda trial: (trial.started_at or "", str(trial.path)))
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def case_results(
|
|
190
|
+
trials: Iterable[HarborTrial],
|
|
191
|
+
) -> dict[tuple[str, str, str], tuple[int, int]]:
|
|
192
|
+
"""Count the passed and the scored trials of each case: a task, an agent and a model.
|
|
193
|
+
|
|
194
|
+
The keys are `(task, agent, model)`. A trial without a reward does not count, so a
|
|
195
|
+
case without a scored trial is absent.
|
|
196
|
+
"""
|
|
197
|
+
cases: dict[tuple[str, str, str], tuple[int, int]] = {}
|
|
198
|
+
for trial in trials:
|
|
199
|
+
if not trial.is_scored:
|
|
200
|
+
continue
|
|
201
|
+
key = (trial.task, trial.agent, trial.model)
|
|
202
|
+
passed, runs = cases.get(key, (0, 0))
|
|
203
|
+
cases[key] = (passed + trial.is_passed, runs + 1)
|
|
204
|
+
return cases
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def summarize(trials: Iterable[HarborTrial]) -> RunSummary:
|
|
208
|
+
"""Summarize a set of trials: counts, the pass rate with its interval, and the cost."""
|
|
209
|
+
trials = tuple(trials)
|
|
210
|
+
cases = case_results(trials)
|
|
211
|
+
scored = sum(trial.is_scored for trial in trials)
|
|
212
|
+
passed = sum(trial.is_passed for trial in trials)
|
|
213
|
+
costs = [trial.cost_usd for trial in trials if trial.cost_usd is not None]
|
|
214
|
+
cost = sum(costs) if costs else None
|
|
215
|
+
starts = [trial.started_at for trial in trials if trial.started_at]
|
|
216
|
+
ends = [trial.finished_at for trial in trials if trial.finished_at]
|
|
217
|
+
return RunSummary(
|
|
218
|
+
trials=len(trials),
|
|
219
|
+
scored=scored,
|
|
220
|
+
passed=passed,
|
|
221
|
+
errors=sum(trial.error is not None for trial in trials),
|
|
222
|
+
cases=len(cases),
|
|
223
|
+
pass_rate=passed / scored if scored else None,
|
|
224
|
+
pass_rate_ci95=cluster_interval(list(cases.values())),
|
|
225
|
+
input_tokens=sum(trial.input_tokens or 0 for trial in trials),
|
|
226
|
+
cache_tokens=sum(trial.cache_tokens or 0 for trial in trials),
|
|
227
|
+
output_tokens=sum(trial.output_tokens or 0 for trial in trials),
|
|
228
|
+
cost_usd=cost,
|
|
229
|
+
cost_per_pass_usd=cost / passed if cost is not None and passed else None,
|
|
230
|
+
tasks=tuple(sorted({trial.task for trial in trials})),
|
|
231
|
+
agents=tuple(sorted({trial.agent for trial in trials})),
|
|
232
|
+
models=tuple(sorted({trial.model for trial in trials})),
|
|
233
|
+
started_at=min(starts) if starts else None,
|
|
234
|
+
finished_at=max(ends) if ends else None,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _file_sha256(path: Path) -> str:
|
|
239
|
+
digest = hashlib.sha256()
|
|
240
|
+
with path.open("rb") as stream:
|
|
241
|
+
for block in iter(lambda: stream.read(1 << 20), b""):
|
|
242
|
+
digest.update(block)
|
|
243
|
+
return digest.hexdigest()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def seal_rows(trials: Iterable[HarborTrial], root: Path | str) -> list[dict[str, Any]]:
|
|
247
|
+
"""Return one seal row for each trial, with the task checksum and the result digest.
|
|
248
|
+
|
|
249
|
+
`result` is the path of the trial `result.json`, relative to `root`, and
|
|
250
|
+
`result_sha256` is the sha256 of that file.
|
|
251
|
+
"""
|
|
252
|
+
root = Path(root)
|
|
253
|
+
return [
|
|
254
|
+
{
|
|
255
|
+
"agent": trial.agent,
|
|
256
|
+
"model": trial.model,
|
|
257
|
+
"task": trial.task,
|
|
258
|
+
"reward": trial.reward,
|
|
259
|
+
"error": trial.error,
|
|
260
|
+
"input": trial.input_tokens,
|
|
261
|
+
"cache": trial.cache_tokens,
|
|
262
|
+
"output": trial.output_tokens,
|
|
263
|
+
"agent_version": trial.agent_version,
|
|
264
|
+
"task_checksum": trial.task_checksum,
|
|
265
|
+
"started_at": trial.started_at,
|
|
266
|
+
"finished_at": trial.finished_at,
|
|
267
|
+
"result": trial.path.relative_to(root).as_posix(),
|
|
268
|
+
"result_sha256": _file_sha256(trial.path),
|
|
269
|
+
}
|
|
270
|
+
for trial in sorted(
|
|
271
|
+
trials,
|
|
272
|
+
key=lambda trial: (trial.agent, trial.model, trial.task, str(trial.path)),
|
|
273
|
+
)
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def seal_digest(body: Mapping[str, Any]) -> str:
|
|
278
|
+
"""Return the sha256 of a seal body with sorted keys, without its `digest` key."""
|
|
279
|
+
content = {key: value for key, value in body.items() if key != "digest"}
|
|
280
|
+
return hashlib.sha256(json.dumps(content, sort_keys=True).encode()).hexdigest()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def seal_round(
|
|
284
|
+
round_name: str,
|
|
285
|
+
runs: Sequence[Mapping[str, Any]],
|
|
286
|
+
trials: Iterable[HarborTrial],
|
|
287
|
+
root: Path | str,
|
|
288
|
+
sealed_at: str | None = None,
|
|
289
|
+
) -> dict[str, Any]:
|
|
290
|
+
"""Return the seal of a round: its run records, one row for each trial and a digest.
|
|
291
|
+
|
|
292
|
+
Each run record holds the commit, the pins, the profiles and the artifact sha256 of
|
|
293
|
+
one run. The rows hold the task checksums and the result digests. `sealed_at`
|
|
294
|
+
defaults to the current UTC time.
|
|
295
|
+
"""
|
|
296
|
+
body: dict[str, Any] = {
|
|
297
|
+
"round": round_name,
|
|
298
|
+
"sealed_at": sealed_at or time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
299
|
+
"runs": [dict(run) for run in runs],
|
|
300
|
+
"rows": seal_rows(trials, root),
|
|
301
|
+
}
|
|
302
|
+
body["digest"] = seal_digest(body)
|
|
303
|
+
return body
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Statistics for evaluation results: intervals, power, paired tests and multiple tests.
|
|
2
|
+
|
|
3
|
+
Each rate, interval and change is a fraction from 0 to 1. A unit is the
|
|
4
|
+
`(passed, runs)` count of one case, for example one task on one route. The repeats of
|
|
5
|
+
one unit are not independent, so the cluster interval and the power use an effective
|
|
6
|
+
size between the unit count and the run count.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
from collections import Counter
|
|
13
|
+
from collections.abc import Hashable, Sequence
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Z95",
|
|
17
|
+
"Z_POWER",
|
|
18
|
+
"benjamini_hochberg",
|
|
19
|
+
"cluster_interval",
|
|
20
|
+
"cohen_kappa",
|
|
21
|
+
"detectable_change",
|
|
22
|
+
"effective_size",
|
|
23
|
+
"mcnemar_exact",
|
|
24
|
+
"paired_change",
|
|
25
|
+
"wilson_interval",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
Z95 = 1.959964
|
|
29
|
+
"""The normal quantile of a two-sided test at the 5% level."""
|
|
30
|
+
|
|
31
|
+
Z_POWER = 0.841621
|
|
32
|
+
"""The normal quantile of 80% power."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def wilson_interval(
|
|
36
|
+
passed: int, runs: int, size: float | None = None
|
|
37
|
+
) -> tuple[float, float] | None:
|
|
38
|
+
"""Return the 95% Wilson interval of a pass rate, or None without runs.
|
|
39
|
+
|
|
40
|
+
`size` replaces the run count when the runs are not independent.
|
|
41
|
+
"""
|
|
42
|
+
size = runs if size is None else size
|
|
43
|
+
if not runs or size <= 0:
|
|
44
|
+
return None
|
|
45
|
+
rate = passed / runs
|
|
46
|
+
spread = Z95**2 / size
|
|
47
|
+
center = (rate + spread / 2) / (1 + spread)
|
|
48
|
+
half = Z95 * math.sqrt(rate * (1 - rate) / size + spread / size / 4) / (1 + spread)
|
|
49
|
+
return max(0.0, center - half), min(1.0, center + half)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def effective_size(units: Sequence[tuple[int, int]]) -> float:
|
|
53
|
+
"""Return the run count of clustered results, made smaller by their design effect.
|
|
54
|
+
|
|
55
|
+
Each unit is `(passed, runs)` of one case. The size stays between the unit count
|
|
56
|
+
and the run count. Without any variation, it is the unit count.
|
|
57
|
+
"""
|
|
58
|
+
units = [(passed, runs) for passed, runs in units if runs]
|
|
59
|
+
count = len(units)
|
|
60
|
+
total = sum(runs for _, runs in units)
|
|
61
|
+
if count < 2 or total == count:
|
|
62
|
+
return float(total)
|
|
63
|
+
rate = sum(passed for passed, _ in units) / total
|
|
64
|
+
if rate in (0, 1):
|
|
65
|
+
return float(count)
|
|
66
|
+
spread = sum((passed - rate * runs) ** 2 for passed, runs in units)
|
|
67
|
+
variance = count / (count - 1) * spread / total**2
|
|
68
|
+
if not variance:
|
|
69
|
+
return float(total)
|
|
70
|
+
return min(float(total), max(float(count), rate * (1 - rate) / variance))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cluster_interval(units: Sequence[tuple[int, int]]) -> tuple[float, float] | None:
|
|
74
|
+
"""Return the 95% interval of the pooled pass rate of clustered units, or None."""
|
|
75
|
+
passed = sum(passed for passed, _ in units)
|
|
76
|
+
runs = sum(runs for _, runs in units)
|
|
77
|
+
return wilson_interval(passed, runs, effective_size(units))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def detectable_change(units: Sequence[tuple[int, int]]) -> float | None:
|
|
81
|
+
"""Return the smallest change of the pass rate that a second run of this size detects.
|
|
82
|
+
|
|
83
|
+
The test compares two independent runs of the same size, two-sided at the 5% level,
|
|
84
|
+
with 80% power. The change has steps of 0.001. None means that there are no runs, or
|
|
85
|
+
that no change from 0 to 1 is detectable.
|
|
86
|
+
"""
|
|
87
|
+
total = sum(runs for _, runs in units)
|
|
88
|
+
if not total:
|
|
89
|
+
return None
|
|
90
|
+
rate = sum(passed for passed, _ in units) / total
|
|
91
|
+
size = effective_size(units)
|
|
92
|
+
|
|
93
|
+
def detects(other: float) -> bool:
|
|
94
|
+
pooled = (rate + other) / 2
|
|
95
|
+
noise = Z95 * math.sqrt(2 * pooled * (1 - pooled) / size)
|
|
96
|
+
power = Z_POWER * math.sqrt((rate * (1 - rate) + other * (1 - other)) / size)
|
|
97
|
+
return abs(other - rate) >= noise + power
|
|
98
|
+
|
|
99
|
+
smallest = None
|
|
100
|
+
for sign in (1, -1):
|
|
101
|
+
for step in range(1, 1001):
|
|
102
|
+
other = rate + sign * step / 1000
|
|
103
|
+
if not 0 <= other <= 1:
|
|
104
|
+
break
|
|
105
|
+
if detects(other):
|
|
106
|
+
smallest = step if smallest is None else min(smallest, step)
|
|
107
|
+
break
|
|
108
|
+
return smallest / 1000 if smallest is not None else None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def paired_change(changes: Sequence[float]) -> tuple[tuple[float, float], float]:
|
|
112
|
+
"""Return the Agresti-Min 95% interval of a mean paired change, and its floor.
|
|
113
|
+
|
|
114
|
+
Each change is the candidate pass rate of one case minus its baseline pass rate.
|
|
115
|
+
The floor is the smallest true change that this many cases detect with 80% power.
|
|
116
|
+
"""
|
|
117
|
+
count = len(changes)
|
|
118
|
+
mean = sum(changes) / (count + 2)
|
|
119
|
+
square = (sum(change * change for change in changes) + 1) / (count + 2)
|
|
120
|
+
error = math.sqrt(max(0.0, square - mean * mean) / (count + 2))
|
|
121
|
+
return (mean - Z95 * error, mean + Z95 * error), (Z95 + Z_POWER) * error
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def cohen_kappa(pairs: Sequence[tuple[Hashable, Hashable]]) -> float | None:
|
|
125
|
+
"""Return Cohen's kappa of two raters over the same items, or None when undefined."""
|
|
126
|
+
if not pairs:
|
|
127
|
+
return None
|
|
128
|
+
count = len(pairs)
|
|
129
|
+
observed = sum(first == second for first, second in pairs) / count
|
|
130
|
+
firsts = Counter(first for first, _ in pairs)
|
|
131
|
+
seconds = Counter(second for _, second in pairs)
|
|
132
|
+
expected = sum(firsts[label] * seconds[label] for label in firsts) / count**2
|
|
133
|
+
if expected >= 1:
|
|
134
|
+
return None
|
|
135
|
+
return (observed - expected) / (1 - expected)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def mcnemar_exact(worse: int, better: int) -> float:
|
|
139
|
+
"""Return the two-sided exact McNemar p-value of paired pass and fail results.
|
|
140
|
+
|
|
141
|
+
`worse` counts the cases that pass only in the baseline. `better` counts the cases
|
|
142
|
+
that pass only in the candidate. Cases with the same result on both sides do not
|
|
143
|
+
change the value. Without a changed case, the value is 1.
|
|
144
|
+
"""
|
|
145
|
+
if worse < 0 or better < 0:
|
|
146
|
+
raise ValueError("worse and better must not be negative")
|
|
147
|
+
total = worse + better
|
|
148
|
+
if not total:
|
|
149
|
+
return 1.0
|
|
150
|
+
tail = sum(math.comb(total, count) for count in range(min(worse, better) + 1))
|
|
151
|
+
return min(1.0, 2 * tail / 2**total)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def benjamini_hochberg(p_values: Sequence[float]) -> list[float]:
|
|
155
|
+
"""Return the Benjamini-Hochberg q-value of each p-value, in the input order.
|
|
156
|
+
|
|
157
|
+
A q-value is the smallest false discovery rate at which its test is a discovery.
|
|
158
|
+
A larger p-value never gets a smaller q-value, and each q-value is at most 1.
|
|
159
|
+
"""
|
|
160
|
+
if any(not 0 <= value <= 1 for value in p_values):
|
|
161
|
+
raise ValueError("each p-value must be from 0 to 1")
|
|
162
|
+
count = len(p_values)
|
|
163
|
+
order = sorted(range(count), key=lambda index: p_values[index])
|
|
164
|
+
q_values = [1.0] * count
|
|
165
|
+
smallest = 1.0
|
|
166
|
+
for rank in range(count, 0, -1):
|
|
167
|
+
index = order[rank - 1]
|
|
168
|
+
smallest = min(smallest, p_values[index] * count / rank)
|
|
169
|
+
q_values[index] = smallest
|
|
170
|
+
return q_values
|
{vis_agent-0.2.38.dev7557 → vis_agent-0.2.38.dev7568}/src/blockether/vis/evals/trajectory.py
RENAMED
|
@@ -322,6 +322,24 @@ class ToolCall:
|
|
|
322
322
|
"""The Python source of the block, or None."""
|
|
323
323
|
return _str(self.arguments.get("code"))
|
|
324
324
|
|
|
325
|
+
@property
|
|
326
|
+
def stdout(self) -> str:
|
|
327
|
+
"""The standard output of the block: the start of `output`.
|
|
328
|
+
|
|
329
|
+
The first `stdout_chars` Unicode code points of `output` are the standard
|
|
330
|
+
output. A newline separates them from the error text. Vis leaves a blank
|
|
331
|
+
standard output out of `output`, so a blank or missing standard output gives
|
|
332
|
+
"".
|
|
333
|
+
"""
|
|
334
|
+
size = self.stdout_chars
|
|
335
|
+
text = self.output or ""
|
|
336
|
+
if not size or len(text) < size:
|
|
337
|
+
return ""
|
|
338
|
+
head = text[:size]
|
|
339
|
+
if not head.strip() or (len(text) > size and text[size] != "\n"):
|
|
340
|
+
return ""
|
|
341
|
+
return head
|
|
342
|
+
|
|
325
343
|
@property
|
|
326
344
|
def activity_state(self) -> str | None:
|
|
327
345
|
"""The state of the whole block, or None without Activity."""
|
|
@@ -468,8 +486,8 @@ class Trajectory:
|
|
|
468
486
|
"""One Vis run read from an ATIF trajectory.
|
|
469
487
|
|
|
470
488
|
The typed fields hold what evaluations check. `document` keeps the whole input,
|
|
471
|
-
also the keys that this reader does not know. Child runs
|
|
472
|
-
|
|
489
|
+
also the keys that this reader does not know. Child runs are in
|
|
490
|
+
`subagent_trajectories`.
|
|
473
491
|
"""
|
|
474
492
|
|
|
475
493
|
schema_version: str
|
|
@@ -479,7 +497,7 @@ class Trajectory:
|
|
|
479
497
|
steps: tuple[Step, ...]
|
|
480
498
|
final_metrics: TrajectoryMetrics | None
|
|
481
499
|
extra: Mapping[str, Any]
|
|
482
|
-
|
|
500
|
+
subagent_trajectories: tuple[Trajectory, ...]
|
|
483
501
|
document: Mapping[str, Any]
|
|
484
502
|
|
|
485
503
|
@property
|
|
@@ -553,8 +571,9 @@ class Trajectory:
|
|
|
553
571
|
def final_step(self) -> Step | None:
|
|
554
572
|
"""The step with the final answer, or None when the run has no final answer.
|
|
555
573
|
|
|
556
|
-
|
|
557
|
-
|
|
574
|
+
Vis marks that step with `is_final`. A file without marks, such as one from an
|
|
575
|
+
earlier Vis version, ends with the last agent step, if that step has no code
|
|
576
|
+
blocks and no error.
|
|
558
577
|
"""
|
|
559
578
|
agent_steps = self.agent_steps
|
|
560
579
|
if any("is_final" in step.extra for step in agent_steps):
|
|
@@ -584,7 +603,7 @@ class Trajectory:
|
|
|
584
603
|
operation: Keep only calls of this function, for example `grep`.
|
|
585
604
|
state: Keep only calls with this outcome, for example `failed`.
|
|
586
605
|
|
|
587
|
-
The calls of
|
|
606
|
+
The calls of child runs are in `subagent_trajectories`, not here.
|
|
588
607
|
"""
|
|
589
608
|
return tuple(
|
|
590
609
|
call
|
|
@@ -614,7 +633,9 @@ class Trajectory:
|
|
|
614
633
|
self.final_metrics.to_dict() if self.final_metrics is not None else None
|
|
615
634
|
),
|
|
616
635
|
"steps": [step.to_dict() for step in self.steps],
|
|
617
|
-
"
|
|
636
|
+
"subagent_trajectories": [
|
|
637
|
+
child.to_dict() for child in self.subagent_trajectories
|
|
638
|
+
],
|
|
618
639
|
}
|
|
619
640
|
|
|
620
641
|
|
|
@@ -720,7 +741,7 @@ def _trajectory(
|
|
|
720
741
|
steps=tuple(_step(step, index, strict) for index, step in enumerate(steps)),
|
|
721
742
|
final_metrics=TrajectoryMetrics._from_atif(document.get("final_metrics")),
|
|
722
743
|
extra=_map(document.get("extra")),
|
|
723
|
-
|
|
744
|
+
subagent_trajectories=tuple(
|
|
724
745
|
_trajectory(child, strict, version)
|
|
725
746
|
for child in (children if isinstance(children, tuple) else ())
|
|
726
747
|
if isinstance(child, Mapping)
|
|
@@ -184,6 +184,22 @@ def test_activity_event_uses_the_shared_fixture_and_named_records():
|
|
|
184
184
|
)
|
|
185
185
|
|
|
186
186
|
|
|
187
|
+
def test_activity_rows_carry_every_field_of_the_row_schema():
|
|
188
|
+
# Regression: the engine writes `read_key`, and the SDK row rejected it.
|
|
189
|
+
from dataclasses import fields
|
|
190
|
+
|
|
191
|
+
from blockether.vis.activity import ActivityProjection, ActivityRow
|
|
192
|
+
|
|
193
|
+
properties = _contracts.definition("activity", "row")["properties"]
|
|
194
|
+
assert {field.name for field in fields(ActivityRow)} == set(properties)
|
|
195
|
+
fixture = json.loads((_contracts._DATA / "fixtures/activity.json").read_text())
|
|
196
|
+
row = dict(fixture["rows"][0], read_key="a" * 64)
|
|
197
|
+
wire = dict(fixture, rows=[row, *fixture["rows"][1:]])
|
|
198
|
+
projection = ActivityProjection.from_wire(wire)
|
|
199
|
+
assert projection.rows[0].read_key == "a" * 64
|
|
200
|
+
assert projection.to_wire() == wire
|
|
201
|
+
|
|
202
|
+
|
|
187
203
|
@pytest.mark.parametrize(
|
|
188
204
|
"sample",
|
|
189
205
|
json.loads((_contracts._DATA / "fixtures/activity-cases.json").read_text()),
|