vis-agent 0.3.3.dev7953__tar.gz → 0.3.4.dev7992__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 (154) hide show
  1. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/PKG-INFO +1 -1
  2. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/agents.json +9 -1
  3. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/evals.json +8 -1
  4. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/pyproject.toml +1 -1
  5. vis_agent-0.3.4.dev7992/release-tag +1 -0
  6. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/engine/__init__.py +2 -1
  7. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/engine/_agent.py +8 -2
  8. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/engine/_council.py +19 -4
  9. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/__init__.py +38 -2
  10. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/agent.py +133 -13
  11. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/bench.py +3 -2
  12. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/environment.py +1 -1
  13. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/grade.py +35 -0
  14. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/harbor.py +63 -6
  15. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/labels.py +54 -8
  16. vis_agent-0.3.4.dev7992/src/blockether/vis/evals/lic.py +277 -0
  17. vis_agent-0.3.4.dev7992/src/blockether/vis/evals/longmemeval.py +413 -0
  18. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/repeats.py +33 -13
  19. vis_agent-0.3.4.dev7992/src/blockether/vis/evals/report.py +60 -0
  20. vis_agent-0.3.4.dev7992/src/blockether/vis/evals/reports.py +246 -0
  21. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/stages.py +18 -2
  22. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/stats.py +79 -3
  23. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/trajectory.py +57 -0
  24. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_agent.py +12 -0
  25. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_client.py +5 -1
  26. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_agent.py +53 -5
  27. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_environment.py +13 -0
  28. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_grade.py +20 -0
  29. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_labels.py +38 -0
  30. vis_agent-0.3.4.dev7992/tests/test_evals_lic.py +114 -0
  31. vis_agent-0.3.4.dev7992/tests/test_evals_longmemeval.py +136 -0
  32. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_repeats.py +21 -0
  33. vis_agent-0.3.4.dev7992/tests/test_evals_report.py +127 -0
  34. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_stages.py +5 -0
  35. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_stats.py +84 -1
  36. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_tasks.py +80 -1
  37. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_trajectory.py +31 -0
  38. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_sdk_bootstrap.py +2 -0
  39. vis_agent-0.3.3.dev7953/release-tag +0 -1
  40. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/.gitignore +0 -0
  41. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/LICENSE +0 -0
  42. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/README.md +0 -0
  43. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/activity-arguments.json +0 -0
  44. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/activity-cases.json +0 -0
  45. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/activity-copy.json +0 -0
  46. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/activity-execution.json +0 -0
  47. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/activity-groups.json +0 -0
  48. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/activity-read-session.json +0 -0
  49. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/activity-reads.json +0 -0
  50. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/activity-results.json +0 -0
  51. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/activity-stream.json +0 -0
  52. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/activity-tables.json +0 -0
  53. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/activity.json +0 -0
  54. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/diff.json +0 -0
  55. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/live-primitives.json +0 -0
  56. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/session-health.json +0 -0
  57. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/fixtures/view.json +0 -0
  58. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/activity.json +0 -0
  59. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/automations.json +0 -0
  60. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/common.json +0 -0
  61. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/config.json +0 -0
  62. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/content.json +0 -0
  63. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/council.json +0 -0
  64. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/diff.json +0 -0
  65. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/gateway.json +0 -0
  66. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/improve.json +0 -0
  67. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/provider.json +0 -0
  68. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/rooms.json +0 -0
  69. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/symbol.json +0 -0
  70. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/toggle.json +0 -0
  71. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/contracts/schema/view.json +0 -0
  72. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/examples/greeter/tests/test_greeter.py +0 -0
  73. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/hatch_build.py +0 -0
  74. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/__init__.py +0 -0
  75. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/_contracts.py +0 -0
  76. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/_outside.py +0 -0
  77. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/_wire.py +0 -0
  78. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/activity.py +0 -0
  79. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/engine/_agents.py +0 -0
  80. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/engine/_client.py +0 -0
  81. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/engine/_extensions.py +0 -0
  82. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/engine/_host.py +0 -0
  83. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/engine/_install.py +0 -0
  84. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/engine/_local.py +0 -0
  85. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/engine/_models.py +0 -0
  86. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/_gateway.py +0 -0
  87. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/check.py +0 -0
  88. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/compare.py +0 -0
  89. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/judge-rubric.toml +0 -0
  90. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/judge.py +0 -0
  91. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/leaderboard.py +0 -0
  92. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/evals/rounds.py +0 -0
  93. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/extension.py +0 -0
  94. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/extension_package.py +0 -0
  95. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/py.typed +0 -0
  96. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/rooms.py +0 -0
  97. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/src/blockether/vis/views.py +0 -0
  98. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/conftest.py +0 -0
  99. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/evals_atif.py +0 -0
  100. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_activity.py +0 -0
  101. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_agent_extension_lifecycle.py +0 -0
  102. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_agent_extensions.py +0 -0
  103. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_agents.py +0 -0
  104. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_api_guides.py +0 -0
  105. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_attachments.py +0 -0
  106. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_authoring.py +0 -0
  107. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_catalog.py +0 -0
  108. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_catalog_recipe.py +0 -0
  109. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_ci_report_recipe.py +0 -0
  110. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_client_extensions_engine.py +0 -0
  111. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_code_quality_recipe.py +0 -0
  112. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_contracts.py +0 -0
  113. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_council.py +0 -0
  114. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_declarations.py +0 -0
  115. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_engine.py +0 -0
  116. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_engine_docstrings.py +0 -0
  117. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_bench.py +0 -0
  118. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_check.py +0 -0
  119. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_compare.py +0 -0
  120. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_gateway.py +0 -0
  121. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_guards.py +0 -0
  122. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_harbor.py +0 -0
  123. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_judge.py +0 -0
  124. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_evals_rounds.py +0 -0
  125. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_extension_package.py +0 -0
  126. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_extension_reference.py +0 -0
  127. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_gateway_performance.py +0 -0
  128. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_goals.py +0 -0
  129. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_install.py +0 -0
  130. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_live_primitives.py +0 -0
  131. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_live_wait.py +0 -0
  132. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_local.py +0 -0
  133. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_method_typing.py +0 -0
  134. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_models.py +0 -0
  135. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_monitor_recipe.py +0 -0
  136. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_namespace.py +0 -0
  137. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_outside.py +0 -0
  138. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_providers.py +0 -0
  139. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_read_session.py +0 -0
  140. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_record_arguments.py +0 -0
  141. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_registration.py +0 -0
  142. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_repair_hooks.py +0 -0
  143. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_result_sequence.py +0 -0
  144. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_rooms.py +0 -0
  145. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_scoped_settings.py +0 -0
  146. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_sdk_guide.py +0 -0
  147. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_sdk_typing.py +0 -0
  148. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_shortcuts_live.py +0 -0
  149. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_structured_live.py +0 -0
  150. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_symbol_contract.py +0 -0
  151. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_testing.py +0 -0
  152. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_text_list_arguments.py +0 -0
  153. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_tools.py +0 -0
  154. {vis_agent-0.3.3.dev7953 → vis_agent-0.3.4.dev7992}/tests/test_views.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vis-agent
3
- Version: 0.3.3.dev7953
3
+ Version: 0.3.4.dev7992
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
@@ -192,7 +192,7 @@
192
192
  "additionalProperties": false
193
193
  },
194
194
  "grant": {
195
- "$comment": "Add file rules to the grant of a subagent. A child without write rules writes only the granted rules after a write grant.",
195
+ "$comment": "Add file rules to the grant of a subagent. A child without write rules already writes where its parent writes, so a write grant changes nothing for it. A write rule under a read_only or deny rule of the child fails.",
196
196
  "type": "object",
197
197
  "properties": {
198
198
  "session_id": {
@@ -238,6 +238,10 @@
238
238
  "task": {
239
239
  "$ref": "#/$defs/spawn/properties/task"
240
240
  },
241
+ "files": {
242
+ "$comment": "Replace the grant of the child for the follow-up task.",
243
+ "$ref": "#/$defs/files"
244
+ },
241
245
  "timeout_secs": {
242
246
  "$ref": "#/$defs/wait/properties/timeout_secs"
243
247
  }
@@ -377,6 +381,10 @@
377
381
  "type": "integer",
378
382
  "minimum": 0
379
383
  },
384
+ "question": {
385
+ "type": "string",
386
+ "minLength": 1
387
+ },
380
388
  "usage": {
381
389
  "type": [
382
390
  "object",
@@ -580,7 +580,7 @@
580
580
  "additionalProperties": false
581
581
  },
582
582
  "agreement": {
583
- "description": "The agreement of automatic results with human labels, with Cohen kappa and advice.",
583
+ "description": "The agreement of automatic results with human labels, with Cohen kappa, Krippendorff alpha and advice.",
584
584
  "type": "object",
585
585
  "properties": {
586
586
  "labelled": {
@@ -598,6 +598,12 @@
598
598
  "null"
599
599
  ]
600
600
  },
601
+ "alpha": {
602
+ "type": [
603
+ "number",
604
+ "null"
605
+ ]
606
+ },
601
607
  "disagreements": {
602
608
  "type": "array",
603
609
  "items": {
@@ -612,6 +618,7 @@
612
618
  "labelled",
613
619
  "agreement_percent",
614
620
  "kappa",
621
+ "alpha",
615
622
  "disagreements",
616
623
  "advice"
617
624
  ],
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
8
8
 
9
9
  [project]
10
10
  name = "vis-agent"
11
- version = "0.3.3.dev7953"
11
+ version = "0.3.4.dev7992"
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.12"
@@ -0,0 +1 @@
1
+ beta-a96abba81be1cb50ca4ae87bc76d6453711765db
@@ -4,7 +4,7 @@ For installation, workflows and lifecycle choices, use the
4
4
  [Python SDK guide](https://vis.blockether.com/python-sdk.html).
5
5
  """
6
6
 
7
- from ._agent import Agent, StructuredOutputError
7
+ from ._agent import Agent, StructuredOutputError, final_answer
8
8
  from ._agents import Subagent, SubagentRecord
9
9
  from ._client import (
10
10
  Event,
@@ -70,5 +70,6 @@ __all__ = [
70
70
  "Turn",
71
71
  "VisTimeout",
72
72
  "ensure_installed",
73
+ "final_answer",
73
74
  "install",
74
75
  ]
@@ -239,7 +239,13 @@ def _syntax_message(answer: str, error: json.JSONDecodeError) -> str:
239
239
  return f"not valid JSON: {error.msg} at {where}"
240
240
 
241
241
 
242
- def _final_answer(turn: dict) -> str | None:
242
+ def final_answer(turn: dict) -> str | None:
243
+ """Return the final answer text of the turn record `turn`, or None.
244
+
245
+ A turn record has no `answer` field. The answer is the Markdown of the last
246
+ `prose` block in `turn["content"]`. A turn without that block, or with only
247
+ blank text in it, gives None.
248
+ """
243
249
  content = turn.get("content")
244
250
  if not isinstance(content, list):
245
251
  return None
@@ -397,7 +403,7 @@ def _run(
397
403
  {"turn": turn, "answer": None, "errors": [_turn_problem(turn)]}
398
404
  )
399
405
  raise StructuredOutputError(f"{name} turn did not complete", attempts)
400
- answer = _final_answer(turn)
406
+ answer = final_answer(turn)
401
407
  result, problems = contract.validate(answer)
402
408
  if not problems:
403
409
  return result
@@ -316,15 +316,27 @@ class Council:
316
316
  return self._records("/cancel", {"session_id": session_id}, "cancel")[0]
317
317
 
318
318
  def resume(
319
- self, session_id: str, task: str, *, timeout_secs: int = 240
319
+ self,
320
+ session_id: str,
321
+ task: str,
322
+ *,
323
+ files: dict[str, list[str]] | None = None,
324
+ timeout_secs: int = 240,
320
325
  ) -> SubagentRecord:
321
326
  """Give a finished subagent a follow-up task in its own session, then wait for it.
322
327
 
323
328
  The parent must be active, as for ``spawn``: an idle parent gets
324
329
  ``inactive-session``. The child keeps its whole context. A child without
325
330
  iterations left needs ``extend`` first. A running child needs ``wait``.
331
+ ``files`` replaces the file grant of the child for the follow-up task.
326
332
  """
327
- body = {"session_id": session_id, "task": task, "timeout_secs": timeout_secs}
333
+ body: dict[str, Any] = {
334
+ "session_id": session_id,
335
+ "task": task,
336
+ "timeout_secs": timeout_secs,
337
+ }
338
+ if files is not None:
339
+ body["files"] = files
328
340
  return self._records("/resume", body, "resume", timeout_secs)[0]
329
341
 
330
342
  def extend(self, session_id: str, iterations: int) -> SubagentRecord:
@@ -343,8 +355,11 @@ class Council:
343
355
  def grant(self, session_id: str, files: dict[str, list[str]]) -> SubagentRecord:
344
356
  """Add ``write``, ``read_only`` or ``deny`` file rules to the grant of a subagent.
345
357
 
346
- A grant gives at most your own access. The record has a ``warning`` when an
347
- active sibling can write the same files.
358
+ A grant gives at most your own access. A write rule that the child can already
359
+ write changes nothing. A write rule under a ``read_only`` or ``deny`` rule of the
360
+ child fails: use ``resume`` with ``files`` for a new scope. The record has a
361
+ ``warning`` for unchanged write rules and for an active sibling that can write
362
+ the same files.
348
363
  """
349
364
  body = {"session_id": session_id, "files": files}
350
365
  return self._records("/grant", body, "grant")[0]
@@ -1,7 +1,9 @@
1
1
  """Evaluation tools: write and run Harbor tasks, read the results and compare them.
2
2
 
3
3
  Write a Vis task with `write_task`, and run tasks with the Vis agent with `run_tasks`.
4
- Read one Vis run with `load_trajectory` or `parse_trajectory`. Read the cases of a
4
+ Give `write_task` a list of user messages for a task with several turns.
5
+ Read one Vis run with `load_trajectory` or `parse_trajectory`, and join the turns of
6
+ one session with `merge_trajectories`. Read the cases of a
5
7
  Harbor dataset with `read_tasks`, and the trials of a job or round with `read_trials`.
6
8
  Count the trials with `case_results` or `summarize`. Seal a finished round with
7
9
  `seal_round`. Compare two results with `compare_cases`, or test paired changes with
@@ -10,7 +12,9 @@ Count the trials with `case_results` or `summarize`. Seal a finished round with
10
12
  then score and diagnose it with `bench_report` and `diagnose`. Keep human cause labels
11
13
  with `save_label`. Place a round on a Harbor Hub board with `read_leaderboard`. Check
12
14
  automatic causes against human labels with `agreement`, and judge stages with
13
- `stage_agreement`. For installation and sessions, use the
15
+ `stage_agreement`. Estimate pass@k and pass^k with `pass_at_k` and `pass_all_k`, and
16
+ the agreement of several raters with `krippendorff_alpha`. Build all of it for one
17
+ round with `round_report`, or run `python -m blockether.vis.evals.report`. For installation and sessions, use the
14
18
  [Python SDK guide](https://vis.blockether.com/python-sdk.html).
15
19
  """
16
20
 
@@ -51,19 +55,24 @@ from .harbor import (
51
55
  seal_digest,
52
56
  seal_round,
53
57
  seal_rows,
58
+ split_turns,
54
59
  summarize,
55
60
  write_task,
56
61
  )
57
62
  from .labels import (
63
+ ALPHA_RELIABLE,
64
+ ALPHA_TENTATIVE,
58
65
  KAPPA_REVIEW,
59
66
  KAPPA_TRUST,
60
67
  MIN_LABELS,
61
68
  STAGES,
62
69
  Agreement,
63
70
  agreement,
71
+ alpha_advice,
64
72
  kappa_advice,
65
73
  majority,
66
74
  sample_agreement,
75
+ sample_alpha,
67
76
  )
68
77
  from .leaderboard import (
69
78
  LEADERBOARD_URL,
@@ -75,6 +84,14 @@ from .leaderboard import (
75
84
  standing,
76
85
  )
77
86
  from .repeats import RepeatStats, repeat_stats, task_attempts
87
+ from .reports import (
88
+ REPORT_FILE,
89
+ RepeatRow,
90
+ RoundReport,
91
+ render_report,
92
+ round_report,
93
+ write_report,
94
+ )
78
95
  from .rounds import round_summary
79
96
  from .stages import (
80
97
  StageAgreement,
@@ -91,8 +108,11 @@ from .stats import (
91
108
  cohen_kappa,
92
109
  detectable_change,
93
110
  effective_size,
111
+ krippendorff_alpha,
94
112
  mcnemar_exact,
95
113
  paired_change,
114
+ pass_all_k,
115
+ pass_at_k,
96
116
  wilson_interval,
97
117
  )
98
118
  from .trajectory import (
@@ -103,10 +123,13 @@ from .trajectory import (
103
123
  Trajectory,
104
124
  TrajectoryMetrics,
105
125
  load_trajectory,
126
+ merge_trajectories,
106
127
  parse_trajectory,
107
128
  )
108
129
 
109
130
  __all__ = [
131
+ "ALPHA_RELIABLE",
132
+ "ALPHA_TENTATIVE",
110
133
  "CAUSES",
111
134
  "EARLY_STOP_SHARE",
112
135
  "KAPPA_REVIEW",
@@ -115,6 +138,7 @@ __all__ = [
115
138
  "MIN_LABELS",
116
139
  "NEAR_MISS_RATIO",
117
140
  "NOTE_LIMIT",
141
+ "REPORT_FILE",
118
142
  "STAGES",
119
143
  "Agreement",
120
144
  "BenchReport",
@@ -128,7 +152,9 @@ __all__ = [
128
152
  "Leaderboard",
129
153
  "LeaderboardRow",
130
154
  "OutcomeCount",
155
+ "RepeatRow",
131
156
  "RepeatStats",
157
+ "RoundReport",
132
158
  "RouteTest",
133
159
  "RunSummary",
134
160
  "StageAgreement",
@@ -143,6 +169,7 @@ __all__ = [
143
169
  "VisStanding",
144
170
  "agent_limits",
145
171
  "agreement",
172
+ "alpha_advice",
146
173
  "attempt_cause",
147
174
  "attempt_outcome",
148
175
  "bench_report",
@@ -159,6 +186,7 @@ __all__ = [
159
186
  "judge_errors",
160
187
  "judged_samples",
161
188
  "kappa_advice",
189
+ "krippendorff_alpha",
162
190
  "latest_trials",
163
191
  "leaderboard_row",
164
192
  "load_labels",
@@ -166,21 +194,28 @@ __all__ = [
166
194
  "load_trajectory",
167
195
  "majority",
168
196
  "mcnemar_exact",
197
+ "merge_trajectories",
169
198
  "paired_change",
170
199
  "parse_trajectory",
200
+ "pass_all_k",
201
+ "pass_at_k",
171
202
  "read_leaderboard",
172
203
  "read_tasks",
173
204
  "read_trial",
174
205
  "read_trials",
206
+ "render_report",
175
207
  "repeat_stats",
208
+ "round_report",
176
209
  "round_summary",
177
210
  "run_tasks",
178
211
  "sample_agreement",
212
+ "sample_alpha",
179
213
  "save_label",
180
214
  "save_stage_label",
181
215
  "seal_digest",
182
216
  "seal_round",
183
217
  "seal_rows",
218
+ "split_turns",
184
219
  "stage_agreement",
185
220
  "standing",
186
221
  "summarize",
@@ -188,5 +223,6 @@ __all__ = [
188
223
  "task_attempts",
189
224
  "task_note",
190
225
  "wilson_interval",
226
+ "write_report",
191
227
  "write_task",
192
228
  ]
@@ -11,6 +11,12 @@ directory:
11
11
  - ``stderr.txt``: the error output of the CLI.
12
12
  - ``trajectory.json``: the ATIF trajectory of the session.
13
13
  - ``run.json``: the provider, the model, the exit code and the wall time.
14
+
15
+ A line with only ``<!-- vis:turn -->`` in the instruction starts the next user
16
+ turn. The agent then sends each message as one CLI turn of the same session, with
17
+ ``--session-id``. ``turns/<n>.json`` keeps the trajectory of turn n, and
18
+ ``trajectory.json`` joins all turns. Other Harbor agents get the whole text as one
19
+ prompt.
14
20
  """
15
21
 
16
22
  from __future__ import annotations
@@ -36,9 +42,12 @@ from pydantic import Field
36
42
  from ..engine._install import ensure_installed
37
43
  from .environment import HostEnvironment
38
44
  from .grade import RUN_FILE, STDERR_FILE, TRACE_FILE, TRAJECTORY_FILE
45
+ from .harbor import split_turns
39
46
  from .trajectory import load_trajectory as load_vis_trajectory
47
+ from .trajectory import merge_trajectories
40
48
 
41
49
  DEFAULT_COMMAND = "vis-agent"
50
+ TURNS_DIR = "turns"
42
51
  CLASSPATH_TIMEOUT_SEC = 120
43
52
  _NATIVE_MAGIC = (
44
53
  b"\x7fELF",
@@ -76,6 +85,10 @@ class VisOptions(AgentOptions):
76
85
  command: str | None = Field(
77
86
  default=None, description="Shell prefix that replaces the engine, for tests."
78
87
  )
88
+ append_system: str | None = Field(
89
+ default=None,
90
+ description="File whose text each trial adds to the system prompt.",
91
+ )
79
92
 
80
93
 
81
94
  @functools.cache
@@ -114,12 +127,20 @@ class ProviderUnavailableError(RuntimeError):
114
127
  """The provider refused the turn, so the trial measures nothing about Vis."""
115
128
 
116
129
 
130
+ SCORED_PROVIDER_CODES = frozenset({"provider_context_overflow"})
131
+ """Provider error codes that the agent causes, so the trial keeps its score.
132
+
133
+ A conversation that grows past the context window shows how the agent manages
134
+ its context, for example that it did not fold. Such a trial fails its checks.
135
+ """
136
+
137
+
117
138
  def provider_failure(trace: Path) -> str | None:
118
139
  """Return the provider error that ended the turn in ``trace``, or None.
119
140
 
120
141
  A provider quota or outage ends the turn with an error block whose code
121
142
  starts with ``provider_``. Such a trial is an infrastructure error, not a
122
- wrong answer.
143
+ wrong answer. The codes in ``SCORED_PROVIDER_CODES`` are not returned.
123
144
  """
124
145
  try:
125
146
  lines = trace.read_text(encoding="utf-8").splitlines()
@@ -138,12 +159,21 @@ def provider_failure(trace: Path) -> str | None:
138
159
  if not isinstance(block, dict) or block.get("type") != "error":
139
160
  continue
140
161
  code = str(block.get("code"))
141
- if code.startswith("provider_"):
162
+ if code.startswith("provider_") and code not in SCORED_PROVIDER_CODES:
142
163
  return f"{code}: {block.get('title') or 'provider error'}"
143
164
  return None
144
165
  return None
145
166
 
146
167
 
168
+ def _session_id(trajectory: Path) -> str | None:
169
+ """Return the session id of the trajectory file, or None."""
170
+ try:
171
+ value = json.loads(trajectory.read_text(encoding="utf-8")).get("session_id")
172
+ except (OSError, ValueError, AttributeError):
173
+ return None
174
+ return value if isinstance(value, str) and value else None
175
+
176
+
147
177
  def check_native_binary(path: str) -> None:
148
178
  """Refuse a launcher script: the measurement must use the native image."""
149
179
  with open(path, "rb") as executable:
@@ -165,6 +195,10 @@ class VisAgent(BaseAgent):
165
195
  (``VIS_HARBOR_NATIVE_BIN``), ``source`` (``VIS_HARBOR_SOURCE``). Without them, the
166
196
  agent runs the ``vis-agent`` CLI of the SDK's release. When the installed CLI is
167
197
  missing or another release, setup downloads that release once into the SDK cache.
198
+
199
+ ``append_system`` (``VIS_HARBOR_APPEND_SYSTEM``) names a file. Each trial writes
200
+ its text to ``.vis/APPEND_SYSTEM.md`` in the workspace, so two runs can compare a
201
+ prompt rule on the same tasks. A task that has that file already is an error.
168
202
  """
169
203
 
170
204
  capabilities = AgentCapabilities(atif=True)
@@ -197,6 +231,12 @@ class VisAgent(BaseAgent):
197
231
  source = options.source or self._setting("VIS_HARBOR_SOURCE")
198
232
  self.source = Path(source).resolve() if source else None
199
233
  self.engine_override = options.command
234
+ append_system = options.append_system or self._setting(
235
+ "VIS_HARBOR_APPEND_SYSTEM"
236
+ )
237
+ self.append_system = (
238
+ Path(append_system).read_text(encoding="utf-8") if append_system else None
239
+ )
200
240
  self.classpath: str | None = None
201
241
  self.installed_command: str | None = None
202
242
  self.model_name = f"{self.provider}/{self.model}"
@@ -229,10 +269,12 @@ class VisAgent(BaseAgent):
229
269
  return [*arguments, "--persist"]
230
270
 
231
271
  def shell_command(self) -> str:
232
- """Return the bash command of one trial.
272
+ """Return the bash command of one CLI turn.
233
273
 
234
274
  Paths and the prompt stay in environment variables. Then the path
235
- translation of the host environment cannot change them.
275
+ translation of the host environment cannot change them. A later turn
276
+ continues the session in ``VIS_HARBOR_SESSION_ID``. Each turn appends to the
277
+ trace and the error output.
236
278
  """
237
279
  if self.engine_override:
238
280
  engine = self.engine_override
@@ -246,15 +288,30 @@ class VisAgent(BaseAgent):
246
288
  return (
247
289
  "unset VIS_GATEWAY_URL; "
248
290
  f"{engine} {arguments} "
249
- f'--trajectory "$VIS_HARBOR_AGENT_LOGS/{TRAJECTORY_FILE}" '
291
+ '${VIS_HARBOR_SESSION_ID:+--session-id "$VIS_HARBOR_SESSION_ID"} '
292
+ '--trajectory "$VIS_HARBOR_AGENT_LOGS/$VIS_HARBOR_TRAJECTORY" '
250
293
  '"$VIS_HARBOR_PROMPT" '
251
- f'> "$VIS_HARBOR_AGENT_LOGS/{TRACE_FILE}" '
252
- f'2> "$VIS_HARBOR_AGENT_LOGS/{STDERR_FILE}"'
294
+ f'>> "$VIS_HARBOR_AGENT_LOGS/{TRACE_FILE}" '
295
+ f'2>> "$VIS_HARBOR_AGENT_LOGS/{STDERR_FILE}"'
253
296
  )
254
297
 
255
- def process_env(self, instruction: str, runtime: Path) -> dict[str, str]:
256
- """Return the variables of the CLI process, with a database for this trial."""
298
+ def process_env(
299
+ self,
300
+ instruction: str,
301
+ runtime: Path,
302
+ *,
303
+ trajectory: str = TRAJECTORY_FILE,
304
+ session_id: str | None = None,
305
+ ) -> dict[str, str]:
306
+ """Return the variables of one CLI turn, with a database for this trial.
307
+
308
+ ``trajectory`` is the trajectory path relative to the agent logs. With
309
+ ``session_id``, the turn continues that session.
310
+ """
257
311
  env = dict(self._extra_env)
312
+ env["VIS_HARBOR_TRAJECTORY"] = trajectory
313
+ if session_id:
314
+ env["VIS_HARBOR_SESSION_ID"] = session_id
258
315
  env["VIS_DB_PATH"] = str(runtime / "vis.mdb")
259
316
  env["VIS_EXTENSIONS_DIR"] = self._setting("VIS_EXTENSIONS_DIR") or str(
260
317
  runtime / "extensions"
@@ -291,6 +348,11 @@ class VisAgent(BaseAgent):
291
348
  async def run(
292
349
  self, instruction: str, environment: BaseEnvironment, context: AgentContext
293
350
  ) -> None:
351
+ turns = split_turns(instruction)
352
+ if self.append_system is not None:
353
+ if not isinstance(environment, HostEnvironment):
354
+ raise TypeError("append_system needs the HostEnvironment workspace")
355
+ self._write_append_system(Path(environment.workspace))
294
356
  runtime = Path(tempfile.mkdtemp(prefix="vis_harbor_runtime_"))
295
357
  record = {
296
358
  "provider": self.provider,
@@ -301,23 +363,81 @@ class VisAgent(BaseAgent):
301
363
  "is_cancelled": False,
302
364
  }
303
365
  started = time.monotonic()
366
+ if len(turns) > 1:
367
+ record["turns"] = []
304
368
  try:
305
- result = await environment.exec(
306
- self.shell_command(), env=self.process_env(instruction, runtime)
307
- )
308
- record["exit_code"] = result.return_code
369
+ if len(turns) == 1:
370
+ result = await environment.exec(
371
+ self.shell_command(), env=self.process_env(instruction, runtime)
372
+ )
373
+ record["exit_code"] = result.return_code
374
+ else:
375
+ await self._run_turns(turns, environment, runtime, record)
309
376
  except asyncio.CancelledError:
310
377
  record["is_cancelled"] = True
311
378
  raise
312
379
  finally:
313
380
  record["wall_s"] = round(time.monotonic() - started, 3)
314
381
  self.logs_dir.mkdir(parents=True, exist_ok=True)
382
+ if len(turns) > 1:
383
+ self._join_turns()
315
384
  (self.logs_dir / RUN_FILE).write_text(json.dumps(record, indent=2) + "\n")
316
385
  shutil.rmtree(runtime, ignore_errors=True)
317
386
  self.populate_context_post_run(context)
318
387
  if failure := provider_failure(self.logs_dir / TRACE_FILE):
319
388
  raise ProviderUnavailableError(failure)
320
389
 
390
+ def _write_append_system(self, workspace: Path) -> None:
391
+ target = workspace / ".vis" / "APPEND_SYSTEM.md"
392
+ if target.exists():
393
+ raise ValueError(
394
+ "append_system cannot replace the .vis/APPEND_SYSTEM.md of the task"
395
+ )
396
+ target.parent.mkdir(parents=True, exist_ok=True)
397
+ target.write_text(self.append_system or "", encoding="utf-8")
398
+
399
+ async def _run_turns(
400
+ self,
401
+ turns: list[str],
402
+ environment: BaseEnvironment,
403
+ runtime: Path,
404
+ record: dict,
405
+ ) -> None:
406
+ """Send each message as one turn of one session; stop at a failed turn."""
407
+ (self.logs_dir / TURNS_DIR).mkdir(parents=True, exist_ok=True)
408
+ session_id = None
409
+ for number, message in enumerate(turns, start=1):
410
+ name = f"{TURNS_DIR}/{number}.json"
411
+ env = self.process_env(
412
+ message, runtime, trajectory=name, session_id=session_id
413
+ )
414
+ result = await environment.exec(self.shell_command(), env=env)
415
+ record["exit_code"] = result.return_code
416
+ record["turns"].append({"exit_code": result.return_code})
417
+ if result.return_code != 0 or provider_failure(self.logs_dir / TRACE_FILE):
418
+ return
419
+ if session_id is None:
420
+ session_id = _session_id(self.logs_dir / name)
421
+ if session_id is None:
422
+ raise RuntimeError("the first turn wrote no persisted session id")
423
+
424
+ def _join_turns(self) -> None:
425
+ """Write ``trajectory.json`` from the trajectories of the finished turns."""
426
+ documents = []
427
+ number = 1
428
+ while (path := self.logs_dir / TURNS_DIR / f"{number}.json").is_file():
429
+ try:
430
+ documents.append(json.loads(path.read_text(encoding="utf-8")))
431
+ except (OSError, ValueError) as error:
432
+ self.logger.warning("Cannot read %s: %s", path, error)
433
+ break
434
+ number += 1
435
+ if documents:
436
+ merged = merge_trajectories(documents)
437
+ (self.logs_dir / TRAJECTORY_FILE).write_text(
438
+ json.dumps(merged, indent=2) + "\n", encoding="utf-8"
439
+ )
440
+
321
441
  def populate_context_post_run(self, context: AgentContext) -> None:
322
442
  """Copy the token and cost totals of the trajectory into ``context``."""
323
443
  path = self.logs_dir / TRAJECTORY_FILE
@@ -87,8 +87,9 @@ class BenchReport:
87
87
  `_ci95` value is a 95% Wilson interval in percent. A second round of the same size
88
88
  cannot detect a change below `detectable_change_points`. `attempts_per_task` is the
89
89
  most common scored attempt count of a task. When it is more than one,
90
- `pass_at_k_percent` and `pass_all_k_percent` give the share of tasks that solved at
91
- least one or all of their first attempts, up to that count. Otherwise they are None.
90
+ `pass_at_k_percent` and `pass_all_k_percent` give the unbiased pass@k and pass^k with
91
+ that count as k: the chance that at least one or all of k attempts solve a task.
92
+ Each task uses all its scored attempts. Otherwise they are None.
92
93
  `unstable_tasks` lists tasks with mixed results. `estimated_metered_cost_usd` and
93
94
  `cost_per_solved_usd` use metered API prices. They are not subscription charges.
94
95
  """
@@ -37,7 +37,7 @@ GIT_SETUP = (
37
37
  ("git", "config", "user.email", "t@t.co"),
38
38
  ("git", "config", "user.name", "t"),
39
39
  ("git", "add", "-A"),
40
- ("git", "commit", "-qm", "init"),
40
+ ("git", "commit", "-q", "--allow-empty", "-m", "init"),
41
41
  )
42
42
  _SAFE_PATH = re.compile(r"[\w./-]+")
43
43
  _BEFORE_PATH = r"(?<![\w./-])"
@@ -25,6 +25,8 @@ import hashlib
25
25
  import json
26
26
  import os
27
27
  import re
28
+ import subprocess
29
+ import sys
28
30
  from collections.abc import Iterable, Mapping
29
31
  from dataclasses import dataclass
30
32
  from pathlib import Path
@@ -38,6 +40,7 @@ TRAJECTORY_FILE = "trajectory.json"
38
40
  RUN_FILE = "run.json"
39
41
  REWARD_FILE = "reward.json"
40
42
  GRADE_FILE = "grade.json"
43
+ ANSWER_PROGRAM_TIMEOUT_SEC = 120
41
44
 
42
45
 
43
46
  TERMINAL_ACTIVITY_STATES = {"succeeded", "failed", "cancelled"}
@@ -1040,6 +1043,32 @@ def _workspace_text(work: str, name: str) -> str:
1040
1043
  return ""
1041
1044
 
1042
1045
 
1046
+ def answer_program_problem(program: str, answer: str, work: str) -> str | None:
1047
+ """Run the Python file ``program`` on the final answer; return its failure.
1048
+
1049
+ The program gets the answer on its standard input and runs in the workspace
1050
+ ``work``. Exit code 0 accepts the answer, and the result is None. Otherwise the
1051
+ result names the last line of the program's error output.
1052
+ """
1053
+ try:
1054
+ completed = subprocess.run(
1055
+ [sys.executable, program],
1056
+ input=answer,
1057
+ capture_output=True,
1058
+ text=True,
1059
+ cwd=work,
1060
+ timeout=ANSWER_PROGRAM_TIMEOUT_SEC,
1061
+ check=False,
1062
+ )
1063
+ except subprocess.TimeoutExpired:
1064
+ return f"answer program ran longer than {ANSWER_PROGRAM_TIMEOUT_SEC} s"
1065
+ if completed.returncode == 0:
1066
+ return None
1067
+ lines = (completed.stderr or completed.stdout).strip().splitlines()
1068
+ reason = lines[-1] if lines else f"exit code {completed.returncode}"
1069
+ return f"answer program rejected the answer: {reason}"
1070
+
1071
+
1043
1072
  def grade_trial(
1044
1073
  sc: Mapping[str, Any], work: str | os.PathLike[str], trial: TrialFiles
1045
1074
  ) -> Grade:
@@ -1186,6 +1215,10 @@ def grade_trial(
1186
1215
  for s in sc.get("want_answer") or []:
1187
1216
  if s not in answer:
1188
1217
  checks.fail("answer", f"answer missing {s!r}")
1218
+ if program := sc.get("want_answer_program"):
1219
+ checks.use("answer")
1220
+ if problem := answer_program_problem(program, answer, work):
1221
+ checks.fail("answer", problem)
1189
1222
  if sc.get("want_forms"):
1190
1223
  checks.use("forms")
1191
1224
  for needle in sc.get("want_forms") or []:
@@ -1545,6 +1578,8 @@ def main(argv: list[str] | None = None) -> int:
1545
1578
  if getattr(args, name) is None:
1546
1579
  parser.error(f"--{name.replace('_', '-')} is required outside Vis Harbor")
1547
1580
  checks = json.loads(args.checks.read_text())
1581
+ if program := checks.get("want_answer_program"):
1582
+ checks["want_answer_program"] = str(args.checks.parent / program)
1548
1583
  if args.task:
1549
1584
  checks["id"] = args.task
1550
1585
  grade = grade_trial(checks, args.workspace, load_trial(args.agent_logs))