vis-agent 0.2.41.dev7619__tar.gz → 0.2.42__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 (137) hide show
  1. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/.gitignore +0 -3
  2. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/PKG-INFO +3 -1
  3. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/evals.json +9 -3
  4. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/pyproject.toml +3 -1
  5. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/evals/__init__.py +14 -9
  6. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/evals/_gateway.py +9 -2
  7. vis_agent-0.2.42/src/blockether/vis/evals/agent.py +341 -0
  8. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/evals/bench.py +7 -6
  9. vis_agent-0.2.42/src/blockether/vis/evals/check.py +211 -0
  10. vis_agent-0.2.42/src/blockether/vis/evals/environment.py +253 -0
  11. vis_agent-0.2.42/src/blockether/vis/evals/grade.py +1551 -0
  12. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/evals/harbor.py +68 -0
  13. vis_agent-0.2.42/src/blockether/vis/evals/judge-rubric.toml +22 -0
  14. vis_agent-0.2.42/src/blockether/vis/evals/judge.py +216 -0
  15. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/evals/leaderboard.py +5 -2
  16. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/evals/rounds.py +47 -24
  17. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/conftest.py +10 -0
  18. vis_agent-0.2.42/tests/evals_atif.py +157 -0
  19. vis_agent-0.2.42/tests/test_evals_agent.py +448 -0
  20. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_evals_bench.py +7 -2
  21. vis_agent-0.2.42/tests/test_evals_check.py +113 -0
  22. vis_agent-0.2.42/tests/test_evals_environment.py +225 -0
  23. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_evals_gateway.py +4 -1
  24. vis_agent-0.2.42/tests/test_evals_grade.py +368 -0
  25. vis_agent-0.2.42/tests/test_evals_guards.py +1611 -0
  26. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_evals_harbor.py +27 -0
  27. vis_agent-0.2.42/tests/test_evals_judge.py +145 -0
  28. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_evals_rounds.py +61 -0
  29. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_sdk_typing.py +14 -5
  30. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/LICENSE +0 -0
  31. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/README.md +0 -0
  32. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/fixtures/activity-arguments.json +0 -0
  33. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/fixtures/activity-cases.json +0 -0
  34. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/fixtures/activity-copy.json +0 -0
  35. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/fixtures/activity-execution.json +0 -0
  36. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/fixtures/activity-groups.json +0 -0
  37. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/fixtures/activity-read-session.json +0 -0
  38. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/fixtures/activity-reads.json +0 -0
  39. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/fixtures/activity-results.json +0 -0
  40. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/fixtures/activity-tables.json +0 -0
  41. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/fixtures/activity.json +0 -0
  42. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/fixtures/diff.json +0 -0
  43. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/fixtures/live-primitives.json +0 -0
  44. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/fixtures/session-health.json +0 -0
  45. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/fixtures/view.json +0 -0
  46. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/activity.json +0 -0
  47. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/agents.json +0 -0
  48. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/automations.json +0 -0
  49. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/common.json +0 -0
  50. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/config.json +0 -0
  51. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/content.json +0 -0
  52. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/council.json +0 -0
  53. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/diff.json +0 -0
  54. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/gateway.json +0 -0
  55. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/improve.json +0 -0
  56. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/provider.json +0 -0
  57. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/rooms.json +0 -0
  58. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/symbol.json +0 -0
  59. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/toggle.json +0 -0
  60. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/contracts/schema/view.json +0 -0
  61. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/examples/greeter/tests/test_greeter.py +0 -0
  62. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/hatch_build.py +0 -0
  63. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/__init__.py +0 -0
  64. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/_contracts.py +0 -0
  65. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/_outside.py +0 -0
  66. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/_wire.py +0 -0
  67. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/activity.py +0 -0
  68. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/engine/__init__.py +0 -0
  69. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/engine/_agent.py +0 -0
  70. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/engine/_agents.py +0 -0
  71. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/engine/_client.py +0 -0
  72. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/engine/_council.py +0 -0
  73. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/engine/_extensions.py +0 -0
  74. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/engine/_local.py +0 -0
  75. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/evals/compare.py +0 -0
  76. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/evals/labels.py +0 -0
  77. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/evals/repeats.py +0 -0
  78. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/evals/stages.py +0 -0
  79. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/evals/stats.py +0 -0
  80. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/evals/trajectory.py +0 -0
  81. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/extension.py +0 -0
  82. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/extension_package.py +0 -0
  83. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/py.typed +0 -0
  84. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/rooms.py +0 -0
  85. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/src/blockether/vis/views.py +0 -0
  86. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_activity.py +0 -0
  87. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_agent.py +0 -0
  88. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_agent_extension_lifecycle.py +0 -0
  89. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_agent_extensions.py +0 -0
  90. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_agents.py +0 -0
  91. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_api_guides.py +0 -0
  92. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_attachments.py +0 -0
  93. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_authoring.py +0 -0
  94. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_catalog.py +0 -0
  95. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_catalog_recipe.py +0 -0
  96. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_ci_report_recipe.py +0 -0
  97. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_client.py +0 -0
  98. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_client_extensions_engine.py +0 -0
  99. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_code_quality_recipe.py +0 -0
  100. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_contracts.py +0 -0
  101. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_council.py +0 -0
  102. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_declarations.py +0 -0
  103. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_engine.py +0 -0
  104. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_engine_docstrings.py +0 -0
  105. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_evals_compare.py +0 -0
  106. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_evals_labels.py +0 -0
  107. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_evals_repeats.py +0 -0
  108. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_evals_stages.py +0 -0
  109. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_evals_stats.py +0 -0
  110. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_evals_trajectory.py +0 -0
  111. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_extension_package.py +0 -0
  112. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_extension_reference.py +0 -0
  113. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_gateway_performance.py +0 -0
  114. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_goals.py +0 -0
  115. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_live_primitives.py +0 -0
  116. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_live_wait.py +0 -0
  117. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_local.py +0 -0
  118. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_method_typing.py +0 -0
  119. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_monitor_recipe.py +0 -0
  120. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_namespace.py +0 -0
  121. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_outside.py +0 -0
  122. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_providers.py +0 -0
  123. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_read_session.py +0 -0
  124. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_record_arguments.py +0 -0
  125. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_registration.py +0 -0
  126. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_repair_hooks.py +0 -0
  127. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_result_sequence.py +0 -0
  128. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_rooms.py +0 -0
  129. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_scoped_settings.py +0 -0
  130. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_sdk_bootstrap.py +0 -0
  131. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_sdk_guide.py +0 -0
  132. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_structured_live.py +0 -0
  133. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_symbol_contract.py +0 -0
  134. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_testing.py +0 -0
  135. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_text_list_arguments.py +0 -0
  136. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_tools.py +0 -0
  137. {vis_agent-0.2.41.dev7619 → vis_agent-0.2.42}/tests/test_views.py +0 -0
@@ -117,6 +117,3 @@ own_your_harness.pptx
117
117
  # Call journal written by the e2e schema-probe fixture when it runs from this
118
118
  # checkout instead of a temporary workspace.
119
119
  /schema-calls.jsonl
120
-
121
- # The uv environment of the e2e Harbor project.
122
- /e2e/.venv/
@@ -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
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
@@ -22,6 +22,8 @@ Requires-Dist: pydantic<3,>=2.13.5
22
22
  Requires-Dist: referencing>=0.28.4
23
23
  Provides-Extra: docs
24
24
  Requires-Dist: pdoc==16.0.0; extra == 'docs'
25
+ Provides-Extra: harbor
26
+ Requires-Dist: harbor==0.24.0; (python_version >= '3.12') and extra == 'harbor'
25
27
  Description-Content-Type: text/markdown
26
28
 
27
29
  # vis-agent
@@ -211,10 +211,12 @@
211
211
  "type": "object",
212
212
  "properties": {
213
213
  "package": {
214
- "type": "string"
214
+ "type": "string",
215
+ "description": "The Harbor Hub package of the board, for example terminal-bench/terminal-bench."
215
216
  },
216
217
  "name": {
217
- "type": "string"
218
+ "type": "string",
219
+ "description": "The board name in the package, for example 4-0-0."
218
220
  },
219
221
  "summary": {
220
222
  "type": "object",
@@ -227,10 +229,14 @@
227
229
  ]
228
230
  }
229
231
  },
232
+ "required": [
233
+ "package",
234
+ "name"
235
+ ],
230
236
  "additionalProperties": false
231
237
  },
232
238
  "bench_report": {
233
- "description": "The score of one Terminal-Bench round. Rates are in percent. Intervals are 95% intervals.",
239
+ "description": "The score of one benchmark round. Rates are in percent. Intervals are 95% intervals.",
234
240
  "type": "object",
235
241
  "properties": {
236
242
  "dataset": {
@@ -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"
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"
@@ -27,6 +27,8 @@ classifiers = [
27
27
 
28
28
  [project.optional-dependencies]
29
29
  docs = ["pdoc==16.0.0"]
30
+ # The Harbor agent, host environment, check and judge of `blockether.vis.evals`.
31
+ harbor = ["harbor==0.24.0; python_version >= '3.12'"]
30
32
 
31
33
  [project.urls]
32
34
  Homepage = "https://vis.blockether.com"
@@ -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",
@@ -61,6 +61,13 @@ def _word(body: Mapping, key: str, default: str) -> str:
61
61
  return default if value is None else value
62
62
 
63
63
 
64
+ def _required_text(body: Mapping, key: str) -> str:
65
+ value = _text(body, key)
66
+ if not value:
67
+ raise ValueError(f"`{key}` is required.")
68
+ return value
69
+
70
+
64
71
  def _cases(body: Mapping, key: str) -> dict[tuple[str, ...], tuple[int, int]]:
65
72
  rows = body.get(key)
66
73
  if not isinstance(rows, list):
@@ -133,8 +140,8 @@ def _agreement(body: Mapping):
133
140
  def _leaderboard(body: Mapping):
134
141
  summary = body.get("summary")
135
142
  return read_leaderboard(
136
- _word(body, "package", "terminal-bench/terminal-bench"),
137
- _word(body, "name", "4-0-0"),
143
+ _required_text(body, "package"),
144
+ _required_text(body, "name"),
138
145
  None if summary is None else _mapping(body, "summary"),
139
146
  _text(body, "round_name"),
140
147
  )
@@ -0,0 +1,341 @@
1
+ """Run the Vis CLI as a Harbor agent on the host.
2
+
3
+ Select it with the Harbor agent import path ``blockether.vis.evals.agent:VisAgent``.
4
+ It runs only in ``blockether.vis.evals.environment:HostEnvironment``. Install the
5
+ ``harbor`` extra of the SDK first. Each trial starts one CLI process with its own
6
+ database. With ``--persist`` the
7
+ CLI runs the gateway in the same process, so no shared gateway and no operator
8
+ extension changes the measurement. The agent writes these files to its logs
9
+ directory:
10
+
11
+ - ``trace.jsonl``: the ``--full-trace-json-stream`` output.
12
+ - ``stderr.txt``: the error output of the CLI.
13
+ - ``trajectory.json``: the ATIF trajectory of the session.
14
+ - ``run.json``: the provider, the model, the exit code and the wall time.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import asyncio
20
+ import functools
21
+ import json
22
+ import os
23
+ import shlex
24
+ import shutil
25
+ import subprocess
26
+ import tempfile
27
+ import time
28
+ from pathlib import Path
29
+
30
+ from harbor.agents.base import BaseAgent
31
+ from harbor.agents.capabilities import AgentCapabilities
32
+ from harbor.agents.options import AgentOptions
33
+ from harbor.environments.base import BaseEnvironment
34
+ from harbor.models.agent.context import AgentContext
35
+ from pydantic import Field
36
+
37
+ from .environment import HostEnvironment
38
+ from .grade import RUN_FILE, STDERR_FILE, TRACE_FILE, TRAJECTORY_FILE
39
+ from .trajectory import load_trajectory as load_vis_trajectory
40
+
41
+ DEFAULT_COMMAND = "vis-agent"
42
+ CLASSPATH_TIMEOUT_SEC = 120
43
+ _NATIVE_MAGIC = (
44
+ b"\x7fELF",
45
+ b"\xcf\xfa\xed\xfe",
46
+ b"\xfe\xed\xfa\xcf",
47
+ b"\xca\xfe\xba\xbe",
48
+ )
49
+ _SOURCE_ENGINE = (
50
+ 'cd "$VIS_HARBOR_SOURCE" && "$VIS_HARBOR_ENGINE" -Scp "$VIS_HARBOR_CLASSPATH" '
51
+ '-J-Duser.dir="$VIS_HARBOR_WORKSPACE" -M:vis'
52
+ )
53
+ _NATIVE_ENGINE = '"$VIS_HARBOR_ENGINE" -Duser.dir="$VIS_HARBOR_WORKSPACE"'
54
+ _INSTALLED_ENGINE = '"$VIS_HARBOR_ENGINE"'
55
+
56
+
57
+ class VisOptions(AgentOptions):
58
+ """The options that ``--ak key=value`` sets."""
59
+
60
+ provider: str | None = Field(
61
+ default=None, description="Vis provider. Wins over the provider in --model."
62
+ )
63
+ model: str | None = Field(
64
+ default=None, description="Vis model. Wins over the model in --model."
65
+ )
66
+ reasoning_effort: str | None = Field(
67
+ default=None, description="Value of the CLI option --reasoning-effort."
68
+ )
69
+ source: str | None = Field(
70
+ default=None, description="Vis checkout to run from source with Clojure."
71
+ )
72
+ clojure: str | None = Field(default=None, description="Clojure CLI executable.")
73
+ native_bin: str | None = Field(
74
+ default=None, description="Raw native Vis executable to run."
75
+ )
76
+ command: str | None = Field(
77
+ default=None, description="Shell prefix that replaces the engine, for tests."
78
+ )
79
+
80
+
81
+ @functools.cache
82
+ def source_classpath(clojure: str, source: Path) -> str:
83
+ """Resolve the :vis classpath once, with absolute entries for any user.dir."""
84
+ classpath = subprocess.check_output(
85
+ [clojure, "-Spath", "-A:vis"],
86
+ cwd=source,
87
+ text=True,
88
+ timeout=CLASSPATH_TIMEOUT_SEC,
89
+ ).strip()
90
+ return os.pathsep.join(
91
+ entry if os.path.isabs(entry) else str(source / entry)
92
+ for entry in classpath.split(os.pathsep)
93
+ )
94
+
95
+
96
+ @functools.cache
97
+ def source_commit(source: Path) -> str | None:
98
+ """Return the commit of the checkout ``source``, or None when git cannot tell."""
99
+ try:
100
+ completed = subprocess.run(
101
+ ["git", "rev-parse", "--short=12", "HEAD"],
102
+ cwd=source,
103
+ capture_output=True,
104
+ text=True,
105
+ timeout=30,
106
+ check=True,
107
+ )
108
+ except (OSError, subprocess.SubprocessError):
109
+ return None
110
+ return completed.stdout.strip() or None
111
+
112
+
113
+ class ProviderUnavailableError(RuntimeError):
114
+ """The provider refused the turn, so the trial measures nothing about Vis."""
115
+
116
+
117
+ def provider_failure(trace: Path) -> str | None:
118
+ """Return the provider error that ended the turn in ``trace``, or None.
119
+
120
+ A provider quota or outage ends the turn with an error block whose code
121
+ starts with ``provider_``. Such a trial is an infrastructure error, not a
122
+ wrong answer.
123
+ """
124
+ try:
125
+ lines = trace.read_text(encoding="utf-8").splitlines()
126
+ except OSError:
127
+ return None
128
+ for line in reversed(lines):
129
+ try:
130
+ frame = json.loads(line)
131
+ except ValueError:
132
+ continue
133
+ if not isinstance(frame, dict) or frame.get("event") != "result":
134
+ continue
135
+ payload = frame.get("payload")
136
+ content = payload.get("content") if isinstance(payload, dict) else None
137
+ for block in content if isinstance(content, list) else ():
138
+ if not isinstance(block, dict) or block.get("type") != "error":
139
+ continue
140
+ code = str(block.get("code"))
141
+ if code.startswith("provider_"):
142
+ return f"{code}: {block.get('title') or 'provider error'}"
143
+ return None
144
+ return None
145
+
146
+
147
+ def check_native_binary(path: str) -> None:
148
+ """Refuse a launcher script: the measurement must use the native image."""
149
+ with open(path, "rb") as executable:
150
+ magic = executable.read(4)
151
+ if magic not in _NATIVE_MAGIC:
152
+ raise ValueError("native_bin must be a raw native executable, not a launcher")
153
+
154
+
155
+ class VisAgent(BaseAgent):
156
+ """Harbor agent that runs one Vis CLI session in the host workspace.
157
+
158
+ The provider and the model come from the first source that sets them:
159
+
160
+ 1. The ``provider`` and ``model`` options (``--ak``).
161
+ 2. ``--model provider/model``.
162
+ 3. ``VIS_PROVIDER`` and ``VIS_MODEL`` in ``--ae`` or in the host environment.
163
+
164
+ The engine comes from the first option that is set: ``command``, ``native_bin``
165
+ (``VIS_HARBOR_NATIVE_BIN``), ``source`` (``VIS_HARBOR_SOURCE``). Without them, the
166
+ agent runs the installed ``vis-agent`` CLI.
167
+ """
168
+
169
+ capabilities = AgentCapabilities(atif=True)
170
+ options_model = VisOptions
171
+
172
+ def __init__(self, logs_dir: Path, model_name: str | None = None, **kwargs) -> None:
173
+ super().__init__(logs_dir=logs_dir, model_name=model_name, **kwargs)
174
+ options = self.options
175
+ if not isinstance(options, VisOptions):
176
+ raise TypeError("VisAgent needs VisOptions")
177
+ provider = (
178
+ options.provider
179
+ or self._parsed_model_provider
180
+ or self._setting("VIS_PROVIDER")
181
+ )
182
+ model = options.model or self._parsed_model_name or self._setting("VIS_MODEL")
183
+ if not provider or not model:
184
+ raise ValueError(
185
+ "VisAgent needs a provider and a model: use --model provider/model"
186
+ )
187
+ self.provider, self.model = provider, model
188
+ self.reasoning_effort = options.reasoning_effort or self._setting(
189
+ "VIS_REASONING_EFFORT"
190
+ )
191
+ self.clojure = (
192
+ options.clojure or self._setting("VIS_HARBOR_CLOJURE") or "clojure"
193
+ )
194
+ native_bin = options.native_bin or self._setting("VIS_HARBOR_NATIVE_BIN")
195
+ self.native_bin = os.path.abspath(native_bin) if native_bin else None
196
+ source = options.source or self._setting("VIS_HARBOR_SOURCE")
197
+ self.source = Path(source).resolve() if source else None
198
+ self.engine_override = options.command
199
+ self.classpath: str | None = None
200
+ self.model_name = f"{self.provider}/{self.model}"
201
+ self._init_model_info()
202
+
203
+ @staticmethod
204
+ def name() -> str:
205
+ return "vis"
206
+
207
+ def version(self) -> str | None:
208
+ if self.engine_override or self.native_bin or self.source is None:
209
+ return None
210
+ return source_commit(self.source)
211
+
212
+ def _setting(self, name: str) -> str | None:
213
+ value = self._extra_env.get(name, os.environ.get(name, ""))
214
+ return value.strip() or None
215
+
216
+ def vis_arguments(self) -> list[str]:
217
+ """Return the CLI options before ``--trajectory`` and the prompt."""
218
+ arguments = [
219
+ "--full-trace-json-stream",
220
+ "--provider",
221
+ self.provider,
222
+ "--model",
223
+ self.model,
224
+ ]
225
+ if self.reasoning_effort:
226
+ arguments += ["--reasoning-effort", self.reasoning_effort]
227
+ return [*arguments, "--persist"]
228
+
229
+ def shell_command(self) -> str:
230
+ """Return the bash command of one trial.
231
+
232
+ Paths and the prompt stay in environment variables. Then the path
233
+ translation of the host environment cannot change them.
234
+ """
235
+ if self.engine_override:
236
+ engine = self.engine_override
237
+ elif self.native_bin:
238
+ engine = _NATIVE_ENGINE
239
+ elif self.source is not None:
240
+ engine = _SOURCE_ENGINE
241
+ else:
242
+ engine = _INSTALLED_ENGINE
243
+ arguments = " ".join(shlex.quote(item) for item in self.vis_arguments())
244
+ return (
245
+ "unset VIS_GATEWAY_URL; "
246
+ f"{engine} {arguments} "
247
+ f'--trajectory "$VIS_HARBOR_AGENT_LOGS/{TRAJECTORY_FILE}" '
248
+ '"$VIS_HARBOR_PROMPT" '
249
+ f'> "$VIS_HARBOR_AGENT_LOGS/{TRACE_FILE}" '
250
+ f'2> "$VIS_HARBOR_AGENT_LOGS/{STDERR_FILE}"'
251
+ )
252
+
253
+ def process_env(self, instruction: str, runtime: Path) -> dict[str, str]:
254
+ """Return the variables of the CLI process, with a database for this trial."""
255
+ env = dict(self._extra_env)
256
+ env["VIS_DB_PATH"] = str(runtime / "vis.mdb")
257
+ env["VIS_EXTENSIONS_DIR"] = self._setting("VIS_EXTENSIONS_DIR") or str(
258
+ runtime / "extensions"
259
+ )
260
+ env["VIS_HARBOR_PROMPT"] = instruction
261
+ if self.native_bin:
262
+ env["VIS_HARBOR_ENGINE"] = self.native_bin
263
+ elif self.source is not None:
264
+ env["VIS_HARBOR_ENGINE"] = self.clojure
265
+ env["VIS_HARBOR_SOURCE"] = str(self.source)
266
+ else:
267
+ env["VIS_HARBOR_ENGINE"] = DEFAULT_COMMAND
268
+ if self.classpath is not None:
269
+ env["VIS_HARBOR_CLASSPATH"] = self.classpath
270
+ return env
271
+
272
+ async def setup(self, environment: BaseEnvironment) -> None:
273
+ if not isinstance(environment, HostEnvironment):
274
+ raise TypeError(
275
+ "VisAgent runs only in blockether.vis.evals.environment:HostEnvironment"
276
+ )
277
+ if self.engine_override:
278
+ return
279
+ if self.native_bin:
280
+ check_native_binary(self.native_bin)
281
+ return
282
+ if self.source is not None:
283
+ self.classpath = await asyncio.to_thread(
284
+ source_classpath, self.clojure, self.source
285
+ )
286
+ return
287
+ if shutil.which(DEFAULT_COMMAND) is None:
288
+ raise FileNotFoundError(
289
+ f"{DEFAULT_COMMAND} is not on PATH: install Vis, or set source or native_bin"
290
+ )
291
+
292
+ async def run(
293
+ self, instruction: str, environment: BaseEnvironment, context: AgentContext
294
+ ) -> None:
295
+ runtime = Path(tempfile.mkdtemp(prefix="vis_harbor_runtime_"))
296
+ record = {
297
+ "provider": self.provider,
298
+ "model": self.model,
299
+ "reasoning_effort": self.reasoning_effort,
300
+ "exit_code": None,
301
+ "wall_s": None,
302
+ "is_cancelled": False,
303
+ }
304
+ started = time.monotonic()
305
+ try:
306
+ result = await environment.exec(
307
+ self.shell_command(), env=self.process_env(instruction, runtime)
308
+ )
309
+ record["exit_code"] = result.return_code
310
+ except asyncio.CancelledError:
311
+ record["is_cancelled"] = True
312
+ raise
313
+ finally:
314
+ record["wall_s"] = round(time.monotonic() - started, 3)
315
+ self.logs_dir.mkdir(parents=True, exist_ok=True)
316
+ (self.logs_dir / RUN_FILE).write_text(json.dumps(record, indent=2) + "\n")
317
+ shutil.rmtree(runtime, ignore_errors=True)
318
+ self.populate_context_post_run(context)
319
+ if failure := provider_failure(self.logs_dir / TRACE_FILE):
320
+ raise ProviderUnavailableError(failure)
321
+
322
+ def populate_context_post_run(self, context: AgentContext) -> None:
323
+ """Copy the token and cost totals of the trajectory into ``context``."""
324
+ path = self.logs_dir / TRAJECTORY_FILE
325
+ if not path.is_file():
326
+ return
327
+ # Harbor calls this hook outside the error handling of a trial. An exception
328
+ # here stops the whole job, so a trajectory that cannot be read only loses
329
+ # its totals.
330
+ try:
331
+ trajectory = load_vis_trajectory(path, strict=False)
332
+ except Exception as error:
333
+ self.logger.warning("Cannot read %s: %s", path, error)
334
+ return
335
+ metrics = trajectory.final_metrics
336
+ if metrics is None:
337
+ return
338
+ context.n_input_tokens = metrics.total_prompt_tokens
339
+ context.n_cache_tokens = metrics.total_cached_tokens
340
+ context.n_output_tokens = metrics.total_completion_tokens
341
+ context.cost_usd = metrics.total_cost_usd
@@ -1,4 +1,4 @@
1
- """Score a Terminal-Bench round and find where it loses tasks.
1
+ """Score a Harbor benchmark round and find where it loses tasks.
2
2
 
3
3
  `round_summary` reads a round summary from Harbor job folders: the trials, the task
4
4
  outcomes and the usage of the round. Give that summary to `bench_report` for
@@ -78,7 +78,7 @@ class OutcomeCount:
78
78
 
79
79
  @dataclass(frozen=True)
80
80
  class BenchReport:
81
- """The Terminal-Bench score of one round, with its usage.
81
+ """The benchmark score of one round, with its usage.
82
82
 
83
83
  `pass_rate_percent` divides solved tasks by scored tasks, by the latest scored
84
84
  attempt of each task. `strict_pass_rate_percent` divides them by all dataset tasks,
@@ -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 [