vis-agent 0.2.41.dev7619__tar.gz → 0.2.42.dev7622__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.
Files changed (124) hide show
  1. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/PKG-INFO +1 -1
  2. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/pyproject.toml +1 -1
  3. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/evals/__init__.py +14 -9
  4. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/evals/bench.py +5 -4
  5. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/evals/harbor.py +68 -0
  6. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/evals/rounds.py +43 -16
  7. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_evals_harbor.py +27 -0
  8. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_evals_rounds.py +42 -0
  9. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/.gitignore +0 -0
  10. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/LICENSE +0 -0
  11. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/README.md +0 -0
  12. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/fixtures/activity-arguments.json +0 -0
  13. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/fixtures/activity-cases.json +0 -0
  14. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/fixtures/activity-copy.json +0 -0
  15. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/fixtures/activity-execution.json +0 -0
  16. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/fixtures/activity-groups.json +0 -0
  17. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/fixtures/activity-read-session.json +0 -0
  18. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/fixtures/activity-reads.json +0 -0
  19. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/fixtures/activity-results.json +0 -0
  20. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/fixtures/activity-tables.json +0 -0
  21. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/fixtures/activity.json +0 -0
  22. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/fixtures/diff.json +0 -0
  23. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/fixtures/live-primitives.json +0 -0
  24. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/fixtures/session-health.json +0 -0
  25. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/fixtures/view.json +0 -0
  26. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/activity.json +0 -0
  27. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/agents.json +0 -0
  28. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/automations.json +0 -0
  29. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/common.json +0 -0
  30. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/config.json +0 -0
  31. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/content.json +0 -0
  32. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/council.json +0 -0
  33. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/diff.json +0 -0
  34. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/evals.json +0 -0
  35. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/gateway.json +0 -0
  36. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/improve.json +0 -0
  37. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/provider.json +0 -0
  38. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/rooms.json +0 -0
  39. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/symbol.json +0 -0
  40. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/toggle.json +0 -0
  41. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/contracts/schema/view.json +0 -0
  42. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/examples/greeter/tests/test_greeter.py +0 -0
  43. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/hatch_build.py +0 -0
  44. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/__init__.py +0 -0
  45. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/_contracts.py +0 -0
  46. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/_outside.py +0 -0
  47. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/_wire.py +0 -0
  48. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/activity.py +0 -0
  49. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/engine/__init__.py +0 -0
  50. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/engine/_agent.py +0 -0
  51. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/engine/_agents.py +0 -0
  52. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/engine/_client.py +0 -0
  53. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/engine/_council.py +0 -0
  54. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/engine/_extensions.py +0 -0
  55. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/engine/_local.py +0 -0
  56. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/evals/_gateway.py +0 -0
  57. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/evals/compare.py +0 -0
  58. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/evals/labels.py +0 -0
  59. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/evals/leaderboard.py +0 -0
  60. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/evals/repeats.py +0 -0
  61. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/evals/stages.py +0 -0
  62. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/evals/stats.py +0 -0
  63. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/evals/trajectory.py +0 -0
  64. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/extension.py +0 -0
  65. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/extension_package.py +0 -0
  66. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/py.typed +0 -0
  67. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/rooms.py +0 -0
  68. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/src/blockether/vis/views.py +0 -0
  69. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/conftest.py +0 -0
  70. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_activity.py +0 -0
  71. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_agent.py +0 -0
  72. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_agent_extension_lifecycle.py +0 -0
  73. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_agent_extensions.py +0 -0
  74. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_agents.py +0 -0
  75. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_api_guides.py +0 -0
  76. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_attachments.py +0 -0
  77. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_authoring.py +0 -0
  78. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_catalog.py +0 -0
  79. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_catalog_recipe.py +0 -0
  80. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_ci_report_recipe.py +0 -0
  81. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_client.py +0 -0
  82. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_client_extensions_engine.py +0 -0
  83. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_code_quality_recipe.py +0 -0
  84. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_contracts.py +0 -0
  85. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_council.py +0 -0
  86. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_declarations.py +0 -0
  87. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_engine.py +0 -0
  88. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_engine_docstrings.py +0 -0
  89. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_evals_bench.py +0 -0
  90. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_evals_compare.py +0 -0
  91. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_evals_gateway.py +0 -0
  92. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_evals_labels.py +0 -0
  93. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_evals_repeats.py +0 -0
  94. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_evals_stages.py +0 -0
  95. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_evals_stats.py +0 -0
  96. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_evals_trajectory.py +0 -0
  97. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_extension_package.py +0 -0
  98. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_extension_reference.py +0 -0
  99. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_gateway_performance.py +0 -0
  100. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_goals.py +0 -0
  101. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_live_primitives.py +0 -0
  102. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_live_wait.py +0 -0
  103. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_local.py +0 -0
  104. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_method_typing.py +0 -0
  105. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_monitor_recipe.py +0 -0
  106. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_namespace.py +0 -0
  107. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_outside.py +0 -0
  108. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_providers.py +0 -0
  109. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_read_session.py +0 -0
  110. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_record_arguments.py +0 -0
  111. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_registration.py +0 -0
  112. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_repair_hooks.py +0 -0
  113. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_result_sequence.py +0 -0
  114. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_rooms.py +0 -0
  115. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_scoped_settings.py +0 -0
  116. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_sdk_bootstrap.py +0 -0
  117. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_sdk_guide.py +0 -0
  118. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_sdk_typing.py +0 -0
  119. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_structured_live.py +0 -0
  120. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_symbol_contract.py +0 -0
  121. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_testing.py +0 -0
  122. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_text_list_arguments.py +0 -0
  123. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/tests/test_tools.py +0 -0
  124. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42.dev7622}/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.41.dev7619
3
+ Version: 0.2.42.dev7622
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.41.dev7619"
11
+ version = "0.2.42.dev7622"
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"
@@ -1,14 +1,15 @@
1
1
  """Evaluation tools: read Vis runs and Harbor jobs, and compare their results.
2
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 two results with
6
- `compare_cases`, or test paired changes with `paired_change` and `mcnemar_exact`.
7
- Score repeated attempts with `task_attempts` and `repeat_stats`. Summarize a
8
- Terminal-Bench round from its Harbor job folders with `round_summary`, then score and
9
- diagnose it with `bench_report` and `diagnose`, and keep human cause
10
- labels with `save_label`. Place a round on a Harbor Hub board with `read_leaderboard`. Check automatic
11
- causes against human labels with `agreement`, and judge stages with `stage_agreement`. For installation and sessions, use the
3
+ Read one Vis run with `load_trajectory` or `parse_trajectory`. Read the cases of a
4
+ Harbor dataset with `read_tasks`, and the trials of a job or round with `read_trials`.
5
+ Count the trials with `case_results` or `summarize`. Seal a finished round with
6
+ `seal_round`. Compare two results with `compare_cases`, or test paired changes with
7
+ `paired_change` and `mcnemar_exact`. Score repeated attempts with `task_attempts` and
8
+ `repeat_stats`. Summarize a round from its Harbor job folders with `round_summary`,
9
+ then score and diagnose it with `bench_report` and `diagnose`. Keep human cause labels
10
+ with `save_label`. Place a round on a Harbor Hub board with `read_leaderboard`. Check
11
+ automatic causes against human labels with `agreement`, and judge stages with
12
+ `stage_agreement`. For installation and sessions, use the
12
13
  [Python SDK guide](https://vis.blockether.com/python-sdk.html).
13
14
  """
14
15
 
@@ -38,9 +39,11 @@ from .bench import (
38
39
  )
39
40
  from .compare import CaseChange, CaseComparison, RouteTest, compare_cases
40
41
  from .harbor import (
42
+ HarborTask,
41
43
  HarborTrial,
42
44
  RunSummary,
43
45
  case_results,
46
+ read_tasks,
44
47
  read_trial,
45
48
  read_trials,
46
49
  seal_digest,
@@ -117,6 +120,7 @@ __all__ = [
117
120
  "Diagnosis",
118
121
  "Finding",
119
122
  "FunctionCall",
123
+ "HarborTask",
120
124
  "HarborTrial",
121
125
  "Leaderboard",
122
126
  "LeaderboardRow",
@@ -162,6 +166,7 @@ __all__ = [
162
166
  "paired_change",
163
167
  "parse_trajectory",
164
168
  "read_leaderboard",
169
+ "read_tasks",
165
170
  "read_trial",
166
171
  "read_trials",
167
172
  "repeat_stats",
@@ -359,7 +359,7 @@ def attempt_cause(trial: Mapping, summary: Mapping, limits: Mapping[str, float])
359
359
  """Give the `diagnose` cause of one attempt, or `solved`.
360
360
 
361
361
  The causes are `dead_tools`, `agent_errors`, `agent_timeouts`, `unscored`,
362
- `early_stops` and `other`.
362
+ `early_stops` and `other`. Empty `limits` use the `agent_limits` of the summary.
363
363
  """
364
364
  outcome = attempt_outcome(trial)
365
365
  if outcome == "solved":
@@ -373,7 +373,7 @@ def attempt_cause(trial: Mapping, summary: Mapping, limits: Mapping[str, float])
373
373
  if outcome == "unscored":
374
374
  return "unscored"
375
375
  seconds = trial.get("agent_seconds")
376
- limit = limits.get(_short(trial.get("task")))
376
+ limit = (limits or summary.get("agent_limits") or {}).get(_short(trial.get("task")))
377
377
  if seconds and limit and seconds / limit <= EARLY_STOP_SHARE:
378
378
  return "early_stops"
379
379
  return "other"
@@ -430,7 +430,8 @@ def diagnose(
430
430
  """Find where a round loses tasks, from its `round_summary` data.
431
431
 
432
432
  `limits` gives the agent time limit of each task, from `agent_limits`. Without
433
- limits, no attempt counts as an early stop. `labels` maps a trial name to a human
433
+ limits, it uses the `agent_limits` of the summary, and without those no attempt
434
+ counts as an early stop. `labels` maps a trial name to a human
434
435
  label with a `cause` value.
435
436
  """
436
437
  outcomes = {
@@ -440,7 +441,7 @@ def diagnose(
440
441
  scored = int(summary.get("scored_tasks") or 0)
441
442
  solved = len(outcomes.get("solved", ()))
442
443
  latest = latest_trials(summary)
443
- limits = limits or {}
444
+ limits = limits or summary.get("agent_limits") or {}
444
445
 
445
446
  def notes(outcome: str) -> list[TaskNote]:
446
447
  return [
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
  import hashlib
13
13
  import json
14
14
  import time
15
+ import tomllib
15
16
  from collections.abc import Iterable, Mapping, Sequence
16
17
  from dataclasses import dataclass
17
18
  from pathlib import Path
@@ -21,9 +22,11 @@ from typing import Any
21
22
  from .stats import cluster_interval
22
23
 
23
24
  __all__ = [
25
+ "HarborTask",
24
26
  "HarborTrial",
25
27
  "RunSummary",
26
28
  "case_results",
29
+ "read_tasks",
27
30
  "read_trial",
28
31
  "read_trials",
29
32
  "seal_digest",
@@ -33,6 +36,8 @@ __all__ = [
33
36
  ]
34
37
 
35
38
  RESULT_FILE = "result.json"
39
+ TASK_FILE = "task.toml"
40
+ INSTRUCTION_FILE = "instruction.md"
36
41
 
37
42
 
38
43
  @dataclass(frozen=True, slots=True)
@@ -301,3 +306,66 @@ def seal_round(
301
306
  }
302
307
  body["digest"] = seal_digest(body)
303
308
  return body
309
+
310
+
311
+ @dataclass(frozen=True, slots=True)
312
+ class HarborTask:
313
+ """One Harbor task, read from its `task.toml` and `instruction.md`.
314
+
315
+ `name` is the task folder name, as in `case_results`. `instruction` is the prompt
316
+ that the agent gets. The timeouts are in seconds, or None when `task.toml` gives
317
+ none. `gpus` is the GPU count that the environment needs. `metadata` holds the
318
+ `[metadata]` table, for example the `lang` of an e2e scenario.
319
+ """
320
+
321
+ name: str
322
+ instruction: str
323
+ agent_timeout_sec: float | None
324
+ verifier_timeout_sec: float | None
325
+ gpus: int
326
+ metadata: Mapping[str, Any]
327
+ path: Path
328
+
329
+
330
+ def _seconds(table: object) -> float | None:
331
+ value = table.get("timeout_sec") if isinstance(table, Mapping) else None
332
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
333
+ return None
334
+ return float(value)
335
+
336
+
337
+ def read_tasks(dataset: Path | str) -> tuple[HarborTask, ...]:
338
+ """Read the tasks of a Harbor dataset directory, sorted by name.
339
+
340
+ Each task is a folder with a `task.toml`. Other folders do not count. A directory
341
+ that does not exist raises `FileNotFoundError`. Compile the e2e scenarios into a
342
+ dataset with `python -m vis_harbor.dataset --out DIR`, then read it here.
343
+ """
344
+ root = Path(dataset)
345
+ if not root.is_dir():
346
+ raise FileNotFoundError(f"no Harbor dataset directory at {root}")
347
+ tasks = []
348
+ for config in sorted(root.glob(f"*/{TASK_FILE}")):
349
+ info = tomllib.loads(config.read_text(encoding="utf-8"))
350
+ instruction = config.parent / INSTRUCTION_FILE
351
+ environment = info.get("environment")
352
+ gpus = environment.get("gpus") if isinstance(environment, dict) else None
353
+ metadata = info.get("metadata")
354
+ tasks.append(
355
+ HarborTask(
356
+ name=config.parent.name,
357
+ instruction=instruction.read_text(encoding="utf-8")
358
+ if instruction.is_file()
359
+ else "",
360
+ agent_timeout_sec=_seconds(info.get("agent")),
361
+ verifier_timeout_sec=_seconds(info.get("verifier")),
362
+ gpus=gpus
363
+ if isinstance(gpus, int) and not isinstance(gpus, bool)
364
+ else 0,
365
+ metadata=MappingProxyType(
366
+ dict(metadata) if isinstance(metadata, dict) else {}
367
+ ),
368
+ path=config.parent,
369
+ )
370
+ )
371
+ return tuple(tasks)
@@ -16,13 +16,12 @@ from collections import Counter
16
16
  from datetime import datetime
17
17
  from pathlib import Path
18
18
 
19
- from .bench import attempt_outcome
19
+ from .bench import agent_limits, attempt_outcome
20
20
  from .trajectory import Step, Trajectory, load_trajectory
21
21
 
22
22
  __all__ = ["round_summary"]
23
23
 
24
24
  _TRAJECTORY = "agent/trajectory.json"
25
- _PREFIX = "terminal-bench/"
26
25
  _KIND = re.compile(r"[A-Za-z0-9_.:-]{1,100}")
27
26
 
28
27
 
@@ -148,8 +147,23 @@ def _tests(path: Path) -> dict | None:
148
147
  return {key: summary[key] for key in keys if _count(summary.get(key))} or None
149
148
 
150
149
 
151
- def _trial(result_path: Path, root: Path, model: str | None) -> dict:
152
- result = json.loads(result_path.read_text(encoding="utf-8"))
150
+ def _results(jobs: Path) -> list[tuple[Path, dict]]:
151
+ """Read each trial `result.json` one or two levels below `jobs`, in path order.
152
+
153
+ A job writes its own `result.json` without `trial_name`. It is not a trial.
154
+ """
155
+ found = []
156
+ for path in sorted({*jobs.glob("*/result.json"), *jobs.glob("*/*/result.json")}):
157
+ try:
158
+ result = json.loads(path.read_text(encoding="utf-8"))
159
+ except (OSError, ValueError):
160
+ continue
161
+ if isinstance(result, dict) and result.get("trial_name"):
162
+ found.append((path, result))
163
+ return found
164
+
165
+
166
+ def _trial(result_path: Path, result: dict, root: Path, model: str | None) -> dict:
153
167
  folder = result_path.parent
154
168
  agent = result.get("agent_result") or {}
155
169
  verifier = result.get("verifier_result") or {}
@@ -207,6 +221,7 @@ def _trial(result_path: Path, root: Path, model: str | None) -> dict:
207
221
  check = result.get("verifier") or {}
208
222
  return {
209
223
  "job": folder.parent.name,
224
+ "dataset": result.get("source"),
210
225
  "task": result.get("task_name"),
211
226
  "trial": result.get("trial_name"),
212
227
  "status": status,
@@ -253,7 +268,16 @@ def _usage_totals(trials: list[dict]) -> dict:
253
268
 
254
269
 
255
270
  def _name(task) -> str | None:
256
- return task.removeprefix(_PREFIX) if isinstance(task, str) else None
271
+ """Name a task without its dataset, as `bench_report` and `diagnose` do."""
272
+ return task.rsplit("/", 1)[-1] if isinstance(task, str) and task else None
273
+
274
+
275
+ def _dataset(trials: list[dict], dataset_dir: Path | str | None) -> str:
276
+ """Name the dataset: the most common Harbor `source` of the trials, or the folder."""
277
+ sources = Counter(trial["dataset"] for trial in trials if trial["dataset"])
278
+ if sources:
279
+ return sources.most_common(1)[0][0]
280
+ return Path(dataset_dir).name if dataset_dir is not None else "unknown"
257
281
 
258
282
 
259
283
  def round_summary(
@@ -261,22 +285,24 @@ def round_summary(
261
285
  dataset_dir: Path | str | None = None,
262
286
  *,
263
287
  model: str | None = None,
264
- dataset: str = "terminal-bench/terminal-bench@4.0.0",
288
+ dataset: str | None = None,
265
289
  ) -> dict:
266
290
  """Summarize every trial under `jobs_dir`, as `bench_report` and `diagnose` read it.
267
291
 
268
- `dataset_dir` holds one folder with a `task.toml` for each task. It gives the
269
- total, the GPU tasks and the tasks without an attempt. Without it, the total is
270
- the attempted tasks. `model` is the model name or the `provider/model` route of
271
- the trajectory agent. An attempt of another model does not count. With no
272
- `model`, any model counts. Each task counts once, by its latest scored attempt.
292
+ `jobs_dir` is one Harbor job folder or a folder of jobs. `dataset_dir` holds one
293
+ folder with a `task.toml` for each task. It gives the total, the GPU tasks, the
294
+ tasks without an attempt and the `agent_limits` that `diagnose` uses. Without it,
295
+ the total is the attempted tasks. `dataset` names the dataset. Without it, the
296
+ name is the `source` that Harbor wrote in the trials. `model` is the model name or
297
+ the `provider/model` route of the agent. An attempt of another model does not
298
+ count. With no `model`, any model counts. Each task counts once, by its latest
299
+ scored attempt.
273
300
  """
274
301
  jobs = Path(jobs_dir)
275
302
  root = jobs.parent
276
- trials = [
277
- _trial(path, root, model) for path in sorted(jobs.glob("*/*/result.json"))
278
- ]
303
+ trials = [_trial(path, result, root, model) for path, result in _results(jobs)]
279
304
  attempted = {name for trial in trials if (name := _name(trial["task"]))}
305
+ limits = {} if dataset_dir is None else agent_limits(dataset_dir)
280
306
  if dataset_dir is None:
281
307
  tasks, gpu_tasks = sorted(attempted), []
282
308
  else:
@@ -299,7 +325,7 @@ def round_summary(
299
325
  (trial for trial in trials if trial["scored"] and _name(trial["task"])),
300
326
  key=lambda trial: trial["finished_at"],
301
327
  ):
302
- scored[trial["task"].removeprefix(_PREFIX)] = trial
328
+ scored[str(_name(trial["task"]))] = trial
303
329
  model_tasks = {
304
330
  name
305
331
  for trial in trials
@@ -315,10 +341,11 @@ def round_summary(
315
341
  if streak and streak["iterations"] > 1:
316
342
  repeated.append({"trial": f"{trial['job']}/{trial['trial']}", **streak})
317
343
  return {
318
- "dataset": dataset,
344
+ "dataset": dataset or _dataset(trials, dataset_dir),
319
345
  "total_dataset_tasks": len(tasks),
320
346
  "gpu_required_tasks": gpu_tasks,
321
347
  "unattempted_tasks": [task for task in tasks if task not in attempted],
348
+ "agent_limits": limits,
322
349
  "attempts": len(trials),
323
350
  "verified_attempts": len(verified),
324
351
  "exception_attempts": sum(trial["status"] == "exception" for trial in trials),
@@ -6,6 +6,7 @@ import json
6
6
  import pytest
7
7
  from blockether.vis.evals import (
8
8
  case_results,
9
+ read_tasks,
9
10
  read_trial,
10
11
  read_trials,
11
12
  seal_digest,
@@ -226,3 +227,29 @@ def test_seal_round_has_one_row_for_each_trial_and_a_digest(tmp_path):
226
227
  assert seal["digest"] == seal_digest(seal) == expected
227
228
  seal["rows"][0]["reward"] = 0.0
228
229
  assert seal_digest(seal) != seal["digest"]
230
+
231
+
232
+ def test_read_tasks_reads_the_task_folders_of_a_dataset(tmp_path):
233
+ task = tmp_path / "dataset" / "fix-git"
234
+ task.mkdir(parents=True)
235
+ (task / "task.toml").write_text(
236
+ '[metadata]\nlang = "py"\n[agent]\ntimeout_sec = 600\n'
237
+ "[verifier]\ntimeout_sec = 300.0\n[environment]\ngpus = 1\n"
238
+ )
239
+ (task / "instruction.md").write_text("Fix the repository.")
240
+ bare = tmp_path / "dataset" / "bare"
241
+ bare.mkdir()
242
+ (bare / "task.toml").write_text("")
243
+ (tmp_path / "dataset" / "notes").mkdir()
244
+
245
+ found = {task.name: task for task in read_tasks(tmp_path / "dataset")}
246
+
247
+ assert sorted(found) == ["bare", "fix-git"]
248
+ assert found["fix-git"].instruction == "Fix the repository."
249
+ assert found["fix-git"].agent_timeout_sec == 600.0
250
+ assert found["fix-git"].verifier_timeout_sec == 300.0
251
+ assert found["fix-git"].gpus == 1
252
+ assert found["fix-git"].metadata == {"lang": "py"}
253
+ assert (found["bare"].instruction, found["bare"].agent_timeout_sec) == ("", None)
254
+ with pytest.raises(FileNotFoundError):
255
+ read_tasks(tmp_path / "missing")
@@ -208,3 +208,45 @@ def test_round_summary_reads_the_harbor_model_without_a_trajectory(tmp_path):
208
208
  assert trials["terminal-bench/silent"]["scored"] is False
209
209
  assert trials["terminal-bench/other"]["model_attempt"] is False
210
210
  assert summary["solved_tasks"] == ["solved"]
211
+
212
+
213
+ def test_round_summary_reads_one_e2e_job_folder(tmp_path):
214
+ # An e2e run writes one Harbor job. Its tasks have no dataset prefix, and the job
215
+ # writes its own result.json next to the trial folders.
216
+ job = tmp_path / "e2e-job"
217
+ (job / "quick__1" / "agent").mkdir(parents=True)
218
+ (job / "result.json").write_text(json.dumps({"id": "job", "stats": {}}))
219
+ result = {
220
+ "task_name": "quick",
221
+ "trial_name": "quick__1",
222
+ "source": "vis-e2e",
223
+ "started_at": "2026-01-02T00:00:00Z",
224
+ "finished_at": "2026-01-02T00:02:00Z",
225
+ "agent_execution": {
226
+ "started_at": "2026-01-02T00:00:00Z",
227
+ "finished_at": "2026-01-02T00:01:00Z",
228
+ },
229
+ "agent_info": {
230
+ "name": "vis",
231
+ "model_info": {"name": MODEL, "provider": PROVIDER},
232
+ },
233
+ "agent_result": {"n_input_tokens": 10, "n_output_tokens": 5},
234
+ "verifier_result": {"rewards": {"reward": 0.0}},
235
+ }
236
+ (job / "quick__1" / "result.json").write_text(json.dumps(result))
237
+ tasks = tmp_path / "dataset"
238
+ for name in ("quick", "unrun"):
239
+ (tasks / name).mkdir(parents=True)
240
+ (tasks / name / "task.toml").write_text("[agent]\ntimeout_sec = 600\n")
241
+
242
+ summary = round_summary(job, tasks)
243
+
244
+ assert summary["dataset"] == "vis-e2e"
245
+ assert summary["attempts"] == 1
246
+ assert summary["scored_tasks"] == 1
247
+ assert summary["unattempted_tasks"] == ["unrun"]
248
+ assert summary["agent_limits"] == {"quick": 600.0, "unrun": 600.0}
249
+ assert summary["trials"][0]["dataset"] == "vis-e2e"
250
+ finding = next(f for f in diagnose(summary).findings if f.kind == "early_stops")
251
+ assert finding.tasks[0].budget_used == 0.1
252
+ assert round_summary(job, dataset="scenarios")["dataset"] == "scenarios"