vis-agent 0.2.42.dev7622__tar.gz → 0.2.42.dev7627__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.42.dev7622 → vis_agent-0.2.42.dev7627}/.gitignore +0 -3
  2. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/PKG-INFO +3 -1
  3. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/evals.json +9 -3
  4. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/pyproject.toml +3 -1
  5. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/evals/_gateway.py +9 -2
  6. vis_agent-0.2.42.dev7627/src/blockether/vis/evals/agent.py +341 -0
  7. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/evals/bench.py +2 -2
  8. vis_agent-0.2.42.dev7627/src/blockether/vis/evals/check.py +211 -0
  9. vis_agent-0.2.42.dev7627/src/blockether/vis/evals/environment.py +253 -0
  10. vis_agent-0.2.42.dev7627/src/blockether/vis/evals/grade.py +1551 -0
  11. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/evals/harbor.py +2 -2
  12. vis_agent-0.2.42.dev7627/src/blockether/vis/evals/judge-rubric.toml +22 -0
  13. vis_agent-0.2.42.dev7627/src/blockether/vis/evals/judge.py +216 -0
  14. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/evals/leaderboard.py +5 -2
  15. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/evals/rounds.py +4 -8
  16. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/conftest.py +10 -0
  17. vis_agent-0.2.42.dev7627/tests/evals_atif.py +157 -0
  18. vis_agent-0.2.42.dev7627/tests/test_evals_agent.py +448 -0
  19. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_evals_bench.py +7 -2
  20. vis_agent-0.2.42.dev7627/tests/test_evals_check.py +113 -0
  21. vis_agent-0.2.42.dev7627/tests/test_evals_environment.py +225 -0
  22. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_evals_gateway.py +4 -1
  23. vis_agent-0.2.42.dev7627/tests/test_evals_grade.py +368 -0
  24. vis_agent-0.2.42.dev7627/tests/test_evals_guards.py +1611 -0
  25. vis_agent-0.2.42.dev7627/tests/test_evals_judge.py +145 -0
  26. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_evals_rounds.py +19 -0
  27. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_sdk_typing.py +14 -5
  28. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/LICENSE +0 -0
  29. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/README.md +0 -0
  30. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/fixtures/activity-arguments.json +0 -0
  31. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/fixtures/activity-cases.json +0 -0
  32. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/fixtures/activity-copy.json +0 -0
  33. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/fixtures/activity-execution.json +0 -0
  34. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/fixtures/activity-groups.json +0 -0
  35. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/fixtures/activity-read-session.json +0 -0
  36. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/fixtures/activity-reads.json +0 -0
  37. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/fixtures/activity-results.json +0 -0
  38. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/fixtures/activity-tables.json +0 -0
  39. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/fixtures/activity.json +0 -0
  40. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/fixtures/diff.json +0 -0
  41. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/fixtures/live-primitives.json +0 -0
  42. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/fixtures/session-health.json +0 -0
  43. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/fixtures/view.json +0 -0
  44. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/activity.json +0 -0
  45. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/agents.json +0 -0
  46. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/automations.json +0 -0
  47. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/common.json +0 -0
  48. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/config.json +0 -0
  49. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/content.json +0 -0
  50. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/council.json +0 -0
  51. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/diff.json +0 -0
  52. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/gateway.json +0 -0
  53. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/improve.json +0 -0
  54. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/provider.json +0 -0
  55. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/rooms.json +0 -0
  56. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/symbol.json +0 -0
  57. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/toggle.json +0 -0
  58. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/contracts/schema/view.json +0 -0
  59. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/examples/greeter/tests/test_greeter.py +0 -0
  60. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/hatch_build.py +0 -0
  61. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/__init__.py +0 -0
  62. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/_contracts.py +0 -0
  63. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/_outside.py +0 -0
  64. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/_wire.py +0 -0
  65. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/activity.py +0 -0
  66. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/engine/__init__.py +0 -0
  67. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/engine/_agent.py +0 -0
  68. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/engine/_agents.py +0 -0
  69. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/engine/_client.py +0 -0
  70. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/engine/_council.py +0 -0
  71. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/engine/_extensions.py +0 -0
  72. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/engine/_local.py +0 -0
  73. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/evals/__init__.py +0 -0
  74. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/evals/compare.py +0 -0
  75. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/evals/labels.py +0 -0
  76. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/evals/repeats.py +0 -0
  77. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/evals/stages.py +0 -0
  78. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/evals/stats.py +0 -0
  79. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/evals/trajectory.py +0 -0
  80. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/extension.py +0 -0
  81. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/extension_package.py +0 -0
  82. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/py.typed +0 -0
  83. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/rooms.py +0 -0
  84. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/src/blockether/vis/views.py +0 -0
  85. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_activity.py +0 -0
  86. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_agent.py +0 -0
  87. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_agent_extension_lifecycle.py +0 -0
  88. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_agent_extensions.py +0 -0
  89. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_agents.py +0 -0
  90. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_api_guides.py +0 -0
  91. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_attachments.py +0 -0
  92. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_authoring.py +0 -0
  93. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_catalog.py +0 -0
  94. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_catalog_recipe.py +0 -0
  95. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_ci_report_recipe.py +0 -0
  96. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_client.py +0 -0
  97. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_client_extensions_engine.py +0 -0
  98. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_code_quality_recipe.py +0 -0
  99. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_contracts.py +0 -0
  100. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_council.py +0 -0
  101. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_declarations.py +0 -0
  102. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_engine.py +0 -0
  103. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_engine_docstrings.py +0 -0
  104. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_evals_compare.py +0 -0
  105. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_evals_harbor.py +0 -0
  106. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_evals_labels.py +0 -0
  107. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_evals_repeats.py +0 -0
  108. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_evals_stages.py +0 -0
  109. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_evals_stats.py +0 -0
  110. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_evals_trajectory.py +0 -0
  111. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_extension_package.py +0 -0
  112. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_extension_reference.py +0 -0
  113. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_gateway_performance.py +0 -0
  114. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_goals.py +0 -0
  115. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_live_primitives.py +0 -0
  116. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_live_wait.py +0 -0
  117. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_local.py +0 -0
  118. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_method_typing.py +0 -0
  119. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_monitor_recipe.py +0 -0
  120. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_namespace.py +0 -0
  121. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_outside.py +0 -0
  122. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_providers.py +0 -0
  123. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_read_session.py +0 -0
  124. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_record_arguments.py +0 -0
  125. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_registration.py +0 -0
  126. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_repair_hooks.py +0 -0
  127. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_result_sequence.py +0 -0
  128. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_rooms.py +0 -0
  129. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_scoped_settings.py +0 -0
  130. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_sdk_bootstrap.py +0 -0
  131. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_sdk_guide.py +0 -0
  132. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_structured_live.py +0 -0
  133. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_symbol_contract.py +0 -0
  134. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_testing.py +0 -0
  135. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_text_list_arguments.py +0 -0
  136. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/tests/test_tools.py +0 -0
  137. {vis_agent-0.2.42.dev7622 → vis_agent-0.2.42.dev7627}/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.42.dev7622
3
+ Version: 0.2.42.dev7627
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.42.dev7622"
11
+ version = "0.2.42.dev7627"
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"
@@ -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,
@@ -0,0 +1,211 @@
1
+ """Check Harbor tasks with the ``harbor check`` rubric and the Vis agent.
2
+
3
+ Run it with ``python -m blockether.vis.evals.check TASK_DIR --provider P --model M``.
4
+ ``TASK_DIR`` is one task or a directory of tasks, for example ``e2e`` of the Vis
5
+ repository. Install the ``harbor`` extra first. Each check makes paid model calls.
6
+
7
+ ``harbor check`` runs Claude Code in Docker. This module uses the same rubric,
8
+ prompt and result validation of Harbor. The Vis agent runs each check in the host
9
+ environment, so the check needs no Docker and no Anthropic key. The reviewed task
10
+ is in ``task/`` of the trial workspace, and the agent writes ``check-result.json``
11
+ next to it. The job directory holds ``check_report.json``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import asyncio
18
+ import shutil
19
+ import sys
20
+ import tempfile
21
+ from collections.abc import Sequence
22
+ from datetime import UTC, datetime
23
+ from pathlib import Path
24
+
25
+ from harbor.analyze import checker
26
+ from harbor.analyze.models import (
27
+ QualityCheckResult,
28
+ build_check_response_model,
29
+ load_rubric,
30
+ )
31
+ from harbor.cli.quality_checker.models import CheckReport
32
+ from harbor.models.job.config import JobConfig
33
+ from harbor.models.trial.config import AgentConfig, EnvironmentConfig, TaskConfig
34
+
35
+ from .environment import WORKSPACE_DIRECTORY
36
+
37
+ AGENT = "blockether.vis.evals.agent:VisAgent"
38
+ ENVIRONMENT = "blockether.vis.evals.environment:HostEnvironment"
39
+ DEFAULT_WORKERS = 5
40
+ TASK_DIRECTORY = "task"
41
+ DEFAULT_JOBS = "jobs/vis-check"
42
+ REPORT_FILE = "check_report.json"
43
+ TEST_SCRIPT = """#!/usr/bin/env bash
44
+ # Validate the check result. The host environment sets the VIS_HARBOR_* paths.
45
+ if "$VIS_HARBOR_PYTHON" "$VIS_HARBOR_TESTS/validate.py" check-result.json; then
46
+ echo 1 > "$VIS_HARBOR_VERIFIER_LOGS/reward.txt"
47
+ else
48
+ echo 0 > "$VIS_HARBOR_VERIFIER_LOGS/reward.txt"
49
+ fi
50
+ """
51
+
52
+
53
+ def prompt_template() -> str:
54
+ """Return Harbor's check prompt with the reviewed task in ``task/`` of the workspace."""
55
+ return (
56
+ (checker.PROMPTS_DIR / "check.txt")
57
+ .read_text()
58
+ .replace("{task_path}", TASK_DIRECTORY)
59
+ )
60
+
61
+
62
+ def assemble(task_dir: Path, rubric, response_model, dest: Path) -> Path:
63
+ """Build the check task of ``task_dir`` in ``dest`` for the host environment.
64
+
65
+ Harbor's verifier script writes to the container paths ``/logs`` and ``/tests``.
66
+ The host environment translates only commands, so the wrapper gets a script
67
+ that reads the ``VIS_HARBOR_*`` paths.
68
+ """
69
+ wrapper = checker.assemble_check_task(
70
+ task_dir=task_dir,
71
+ rubric=rubric,
72
+ template=prompt_template(),
73
+ output_schema=response_model.model_json_schema(),
74
+ dest=dest,
75
+ )
76
+ environment = wrapper / "environment"
77
+ workspace = environment / WORKSPACE_DIRECTORY
78
+ workspace.mkdir()
79
+ (environment / TASK_DIRECTORY).rename(workspace / TASK_DIRECTORY)
80
+ script = wrapper / "tests" / "test.sh"
81
+ script.write_text(TEST_SCRIPT)
82
+ script.chmod(0o755)
83
+ return wrapper
84
+
85
+
86
+ def job_config(
87
+ wrappers: Sequence[Path],
88
+ provider: str,
89
+ model: str,
90
+ jobs_dir: Path,
91
+ job_name: str,
92
+ workers: int,
93
+ ) -> JobConfig:
94
+ """Return the Harbor job that checks each wrapper task once with the Vis agent."""
95
+ return JobConfig(
96
+ job_name=job_name,
97
+ jobs_dir=jobs_dir,
98
+ n_concurrent_trials=workers,
99
+ n_attempts=1,
100
+ quiet=True,
101
+ agents=[AgentConfig(import_path=AGENT, model_name=f"{provider}/{model}")],
102
+ environment=EnvironmentConfig(import_path=ENVIRONMENT),
103
+ tasks=[TaskConfig(path=wrapper) for wrapper in wrappers],
104
+ datasets=[],
105
+ )
106
+
107
+
108
+ async def check(
109
+ path: Path,
110
+ provider: str,
111
+ model: str,
112
+ jobs_dir: Path = Path(DEFAULT_JOBS),
113
+ job_name: str | None = None,
114
+ workers: int = DEFAULT_WORKERS,
115
+ ) -> tuple[CheckReport, Path]:
116
+ """Check the task or the tasks in ``path``. Return the report and the job directory."""
117
+ from harbor.job import Job
118
+
119
+ task_dirs = checker._resolve_task_dirs(Path(path), None, None, None)
120
+ rubric = load_rubric(None)
121
+ response_model = build_check_response_model(rubric)
122
+ name = job_name or datetime.now(UTC).strftime("check-%Y%m%d-%H%M%S")
123
+ tmp = Path(tempfile.mkdtemp(prefix="vis-harbor-check-"))
124
+ try:
125
+ names = {}
126
+ wrappers = []
127
+ for task_dir in task_dirs:
128
+ wrapper = assemble(
129
+ task_dir,
130
+ rubric,
131
+ response_model,
132
+ tmp / f"check-{task_dir.resolve().name}",
133
+ )
134
+ wrappers.append(wrapper)
135
+ names[str(wrapper.resolve())] = task_dir.name
136
+ config = job_config(wrappers, provider, model, Path(jobs_dir), name, workers)
137
+ job = await Job.create(config)
138
+ job_result = await job.run()
139
+ results = []
140
+ for trial_result in job_result.trial_results:
141
+ wrapper_path = trial_result.config.task.path
142
+ task_name = names.get(
143
+ str(wrapper_path.resolve()) if wrapper_path else "",
144
+ trial_result.trial_name,
145
+ )
146
+ trial_dir = job.job_dir / trial_result.trial_name
147
+ try:
148
+ result = checker._extract_check_result(
149
+ trial_result, trial_dir, response_model
150
+ )
151
+ result.task_name = task_name
152
+ except (ValueError, RuntimeError) as error:
153
+ result = QualityCheckResult(task_name=task_name, error=str(error))
154
+ results.append(result)
155
+ results.sort(key=lambda result: result.task_name or "")
156
+ report = CheckReport(results=results)
157
+ (job.job_dir / REPORT_FILE).write_text(report.model_dump_json(indent=2))
158
+ return report, job.job_dir
159
+ finally:
160
+ shutil.rmtree(tmp, ignore_errors=True)
161
+
162
+
163
+ def print_report(report: CheckReport, job_dir: Path) -> None:
164
+ """Print the report with the tables of ``harbor check``."""
165
+ from harbor.cli.analyze import _render_check_summary, _render_checks_table, console
166
+
167
+ if len(report.results) == 1:
168
+ result = report.results[0]
169
+ if result.error:
170
+ console.print(f"{result.task_name}: {result.error}")
171
+ else:
172
+ _render_checks_table(
173
+ f"Task Quality Checks: {result.task_name}", result.checks
174
+ )
175
+ else:
176
+ _render_check_summary(report)
177
+ console.print(f"Report: {job_dir / REPORT_FILE}")
178
+
179
+
180
+ def main(argv: Sequence[str] | None = None) -> int:
181
+ """Check the tasks of ``argv``. Return 1 if a check has no valid result."""
182
+ parser = argparse.ArgumentParser(prog="python -m blockether.vis.evals.check")
183
+ parser.add_argument(
184
+ "path", type=Path, help="a task directory or a directory of tasks"
185
+ )
186
+ parser.add_argument("--provider", required=True)
187
+ parser.add_argument("--model", required=True)
188
+ parser.add_argument("--jobs-dir", type=Path, default=Path(DEFAULT_JOBS))
189
+ parser.add_argument("--job-name")
190
+ parser.add_argument("--workers", type=int, default=DEFAULT_WORKERS)
191
+ args = parser.parse_args(argv)
192
+ try:
193
+ report, job_dir = asyncio.run(
194
+ check(
195
+ args.path,
196
+ provider=args.provider,
197
+ model=args.model,
198
+ jobs_dir=args.jobs_dir,
199
+ job_name=args.job_name,
200
+ workers=args.workers,
201
+ )
202
+ )
203
+ except (FileNotFoundError, ValueError) as error:
204
+ print(error, file=sys.stderr)
205
+ return 1
206
+ print_report(report, job_dir)
207
+ return 1 if any(result.error for result in report.results) else 0
208
+
209
+
210
+ if __name__ == "__main__":
211
+ raise SystemExit(main())