vis-agent 0.3.5.dev8266__tar.gz → 0.4.3__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 (157) hide show
  1. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/PKG-INFO +1 -1
  2. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/agents.json +0 -4
  3. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/gateway.json +124 -1
  4. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/view.json +8 -0
  5. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/pyproject.toml +1 -1
  6. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/_contracts.py +16 -0
  7. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/_outside.py +7 -0
  8. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_client.py +1 -1
  9. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/grade.py +19 -30
  10. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/extension.py +49 -8
  11. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/extension_package.py +8 -2
  12. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_engine.py +12 -6
  13. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_guards.py +21 -15
  14. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_live_primitives.py +29 -1
  15. vis_agent-0.4.3/tests/test_private_file.py +77 -0
  16. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_sdk_bootstrap.py +3 -2
  17. vis_agent-0.3.5.dev8266/release-tag +0 -1
  18. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/.gitignore +0 -0
  19. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/LICENSE +0 -0
  20. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/README.md +0 -0
  21. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-arguments.json +0 -0
  22. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-cases.json +0 -0
  23. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-copy.json +0 -0
  24. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-execution.json +0 -0
  25. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-groups.json +0 -0
  26. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-read-session.json +0 -0
  27. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-reads.json +0 -0
  28. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-results.json +0 -0
  29. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-stream.json +0 -0
  30. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity-tables.json +0 -0
  31. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/activity.json +0 -0
  32. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/diff.json +0 -0
  33. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/live-primitives.json +0 -0
  34. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/session-health.json +0 -0
  35. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/fixtures/view.json +0 -0
  36. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/activity.json +0 -0
  37. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/automations.json +0 -0
  38. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/common.json +0 -0
  39. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/config.json +0 -0
  40. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/content.json +0 -0
  41. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/council.json +0 -0
  42. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/diff.json +0 -0
  43. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/evals.json +0 -0
  44. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/provider.json +0 -0
  45. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/symbol.json +0 -0
  46. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/contracts/schema/toggle.json +0 -0
  47. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/examples/greeter/tests/test_greeter.py +0 -0
  48. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/hatch_build.py +0 -0
  49. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/__init__.py +0 -0
  50. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/_wire.py +0 -0
  51. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/activity.py +0 -0
  52. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/__init__.py +0 -0
  53. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_agent.py +0 -0
  54. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_agents.py +0 -0
  55. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_council.py +0 -0
  56. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_extensions.py +0 -0
  57. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_host.py +0 -0
  58. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_install.py +0 -0
  59. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_local.py +0 -0
  60. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_models.py +0 -0
  61. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/engine/_speech.py +0 -0
  62. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/__init__.py +0 -0
  63. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/_gateway.py +0 -0
  64. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/agent.py +0 -0
  65. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/bench.py +0 -0
  66. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/check.py +0 -0
  67. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/compare.py +0 -0
  68. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/environment.py +0 -0
  69. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/harbor.py +0 -0
  70. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/judge-rubric.toml +0 -0
  71. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/judge.py +0 -0
  72. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/labels.py +0 -0
  73. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/leaderboard.py +0 -0
  74. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/lic.py +0 -0
  75. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/longmemeval.py +0 -0
  76. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/mrcr.py +0 -0
  77. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/repeats.py +0 -0
  78. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/report.py +0 -0
  79. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/reports.py +0 -0
  80. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/rounds.py +0 -0
  81. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/stages.py +0 -0
  82. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/stats.py +0 -0
  83. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/evals/trajectory.py +0 -0
  84. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/py.typed +0 -0
  85. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/src/blockether/vis/views.py +0 -0
  86. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/conftest.py +0 -0
  87. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/evals_atif.py +0 -0
  88. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_activity.py +0 -0
  89. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_agent.py +0 -0
  90. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_agent_extension_lifecycle.py +0 -0
  91. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_agent_extensions.py +0 -0
  92. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_agents.py +0 -0
  93. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_api_guides.py +0 -0
  94. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_app_attachments.py +0 -0
  95. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_app_door.py +0 -0
  96. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_attachments.py +0 -0
  97. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_authoring.py +0 -0
  98. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_catalog.py +0 -0
  99. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_catalog_recipe.py +0 -0
  100. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_ci_report_recipe.py +0 -0
  101. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_client.py +0 -0
  102. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_client_extensions_engine.py +0 -0
  103. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_code_quality_recipe.py +0 -0
  104. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_contracts.py +0 -0
  105. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_council.py +0 -0
  106. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_declarations.py +0 -0
  107. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_engine_docstrings.py +0 -0
  108. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_agent.py +0 -0
  109. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_bench.py +0 -0
  110. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_check.py +0 -0
  111. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_compare.py +0 -0
  112. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_environment.py +0 -0
  113. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_gateway.py +0 -0
  114. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_grade.py +0 -0
  115. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_harbor.py +0 -0
  116. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_judge.py +0 -0
  117. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_labels.py +0 -0
  118. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_lic.py +0 -0
  119. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_longmemeval.py +0 -0
  120. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_mrcr.py +0 -0
  121. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_repeats.py +0 -0
  122. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_report.py +0 -0
  123. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_rounds.py +0 -0
  124. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_stages.py +0 -0
  125. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_stats.py +0 -0
  126. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_tasks.py +0 -0
  127. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_evals_trajectory.py +0 -0
  128. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_extension_package.py +0 -0
  129. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_extension_reference.py +0 -0
  130. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_gateway_performance.py +0 -0
  131. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_goals.py +0 -0
  132. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_install.py +0 -0
  133. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_keybindings.py +0 -0
  134. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_live_wait.py +0 -0
  135. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_local.py +0 -0
  136. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_method_typing.py +0 -0
  137. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_models.py +0 -0
  138. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_monitor_recipe.py +0 -0
  139. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_namespace.py +0 -0
  140. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_outside.py +0 -0
  141. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_providers.py +0 -0
  142. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_read_session.py +0 -0
  143. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_record_arguments.py +0 -0
  144. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_registration.py +0 -0
  145. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_repair_hooks.py +0 -0
  146. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_result_sequence.py +0 -0
  147. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_scoped_settings.py +0 -0
  148. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_sdk_guide.py +0 -0
  149. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_sdk_typing.py +0 -0
  150. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_shortcuts_live.py +0 -0
  151. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_speech.py +0 -0
  152. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_structured_live.py +0 -0
  153. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_symbol_contract.py +0 -0
  154. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_testing.py +0 -0
  155. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_text_list_arguments.py +0 -0
  156. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/tests/test_tools.py +0 -0
  157. {vis_agent-0.3.5.dev8266 → vis_agent-0.4.3}/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.5.dev8266
3
+ Version: 0.4.3
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
@@ -638,16 +638,12 @@
638
638
  "type": "integer",
639
639
  "minimum": 1
640
640
  },
641
- "kind": {
642
- "type": "string"
643
- },
644
641
  "content": {
645
642
  "type": "string"
646
643
  }
647
644
  },
648
645
  "required": [
649
646
  "entry_id",
650
- "kind",
651
647
  "content"
652
648
  ],
653
649
  "additionalProperties": false
@@ -967,7 +967,8 @@
967
967
  "operations": {
968
968
  "get": {
969
969
  "request": "none",
970
- "response": "json"
970
+ "response": "json",
971
+ "response_schema": "file_preview"
971
972
  }
972
973
  },
973
974
  "path": "/v1/sessions/:sid/fs/file"
@@ -3735,6 +3736,128 @@
3735
3736
  "pattern": "^[0-9a-f]{64}$"
3736
3737
  }
3737
3738
  }
3739
+ },
3740
+ "preview_image_bytes": {
3741
+ "type": "integer",
3742
+ "minimum": 0,
3743
+ "maximum": 8388608,
3744
+ "description": "Bytes of one picture in a file preview. The picture goes as base64 inside JSON, which is 4/3 larger, and a phone decodes it. A larger picture gets a 413 answer."
3745
+ },
3746
+ "file_preview": {
3747
+ "description": "The answer of GET /v1/sessions/:sid/fs/file. The kind tells what the file holds and which keys come back.",
3748
+ "oneOf": [
3749
+ {
3750
+ "type": "object",
3751
+ "title": "Text",
3752
+ "description": "One window of clipped lines around the line that the client named.",
3753
+ "additionalProperties": false,
3754
+ "required": [
3755
+ "path",
3756
+ "kind",
3757
+ "line",
3758
+ "first_line",
3759
+ "lines",
3760
+ "is_truncated",
3761
+ "size_bytes"
3762
+ ],
3763
+ "properties": {
3764
+ "path": {
3765
+ "type": "string",
3766
+ "minLength": 1
3767
+ },
3768
+ "kind": {
3769
+ "const": "text"
3770
+ },
3771
+ "line": {
3772
+ "type": "integer",
3773
+ "minimum": 1,
3774
+ "description": "The line that the client named, or 1."
3775
+ },
3776
+ "first_line": {
3777
+ "type": "integer",
3778
+ "minimum": 1,
3779
+ "description": "The number of the first line in lines."
3780
+ },
3781
+ "lines": {
3782
+ "type": "array",
3783
+ "items": {
3784
+ "type": "string"
3785
+ }
3786
+ },
3787
+ "is_truncated": {
3788
+ "type": "boolean",
3789
+ "description": "True when the byte cap stopped the read before the window ended."
3790
+ },
3791
+ "size_bytes": {
3792
+ "type": "integer",
3793
+ "minimum": 0
3794
+ }
3795
+ }
3796
+ },
3797
+ {
3798
+ "type": "object",
3799
+ "title": "Image",
3800
+ "description": "A picture that a browser draws, typed by its first bytes, not by its file name.",
3801
+ "additionalProperties": false,
3802
+ "required": [
3803
+ "path",
3804
+ "kind",
3805
+ "media_type",
3806
+ "size_bytes",
3807
+ "data"
3808
+ ],
3809
+ "properties": {
3810
+ "path": {
3811
+ "type": "string",
3812
+ "minLength": 1
3813
+ },
3814
+ "kind": {
3815
+ "const": "image"
3816
+ },
3817
+ "media_type": {
3818
+ "enum": [
3819
+ "image/png",
3820
+ "image/jpeg",
3821
+ "image/gif",
3822
+ "image/webp",
3823
+ "image/avif"
3824
+ ]
3825
+ },
3826
+ "size_bytes": {
3827
+ "$ref": "#/$defs/preview_image_bytes"
3828
+ },
3829
+ "data": {
3830
+ "type": "string",
3831
+ "pattern": "^[A-Za-z0-9+/]*={0,2}$",
3832
+ "description": "The bytes of the picture as standard base64."
3833
+ }
3834
+ }
3835
+ },
3836
+ {
3837
+ "type": "object",
3838
+ "title": "Binary",
3839
+ "description": "Another file with a NUL byte. The gateway does not read it.",
3840
+ "additionalProperties": false,
3841
+ "required": [
3842
+ "path",
3843
+ "kind",
3844
+ "size_bytes"
3845
+ ],
3846
+ "properties": {
3847
+ "path": {
3848
+ "type": "string",
3849
+ "minLength": 1
3850
+ },
3851
+ "kind": {
3852
+ "const": "binary"
3853
+ },
3854
+ "size_bytes": {
3855
+ "type": "integer",
3856
+ "minimum": 0
3857
+ }
3858
+ }
3859
+ }
3860
+ ]
3738
3861
  }
3739
3862
  }
3740
3863
  }
@@ -1493,6 +1493,14 @@
1493
1493
  ],
1494
1494
  "default": "activate"
1495
1495
  },
1496
+ "variant": {
1497
+ "enum": [
1498
+ "primary",
1499
+ "secondary",
1500
+ "destructive"
1501
+ ],
1502
+ "default": "secondary"
1503
+ },
1496
1504
  "is_disabled": {
1497
1505
  "type": "boolean"
1498
1506
  },
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
8
8
 
9
9
  [project]
10
10
  name = "vis-agent"
11
- version = "0.3.5.dev8266"
11
+ version = "0.4.3"
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"
@@ -61,6 +61,22 @@ def live_items(key: str, items: list[dict[str, Any]]) -> list[dict[str, Any]]:
61
61
  return result
62
62
 
63
63
 
64
+ def _button_branch() -> dict[str, Any]:
65
+ return next(
66
+ branch
67
+ for branch in definition("view", "component")["oneOf"]
68
+ if branch["properties"]["type"].get("const") == "button"
69
+ )
70
+
71
+
72
+ def button_variant_error(variant: Any) -> str | None:
73
+ """Answer why a declared button variant is invalid, or None when it is valid."""
74
+ allowed = _button_branch()["properties"]["variant"]["enum"]
75
+ if variant is None or variant in allowed:
76
+ return None
77
+ return f"button variant must be one of {', '.join(allowed)}, not {variant!r}"
78
+
79
+
64
80
  def _defaults(key: str) -> dict[str, Any]:
65
81
  properties = definition("view", key)["properties"]
66
82
  return {
@@ -1076,6 +1076,10 @@ def _live_check_node(node, seen, *, is_declaration=True):
1076
1076
  return f"a {kind} input has no value: only the host holds its handle"
1077
1077
  node_id = node.get("id")
1078
1078
  if not isinstance(node_id, str) or not node_id.strip():
1079
+ if is_group:
1080
+ from .extension import _LIVE_GROUP_NEEDS_ID
1081
+
1082
+ return _LIVE_GROUP_NEEDS_ID
1079
1083
  return f"a {kind} node needs an id"
1080
1084
  if node_id in seen:
1081
1085
  return f"two nodes answer to {node_id!r}"
@@ -1129,6 +1133,9 @@ def _live_check_node(node, seen, *, is_declaration=True):
1129
1133
  return "a button needs a label"
1130
1134
  if is_declaration and "clicks" in node:
1131
1135
  return "button clicks are stamped by the engine"
1136
+ complaint = _contracts.button_variant_error(node.get("variant"))
1137
+ if complaint:
1138
+ return complaint
1132
1139
  node.setdefault("clicks", 0)
1133
1140
  node.setdefault("is_disabled", False)
1134
1141
  if type(node["clicks"]) is not int or node["clicks"] < 0:
@@ -1578,7 +1578,7 @@ class ExecutionLayer(ABC):
1578
1578
  def get_session_fs_file(
1579
1579
  self, sid: str, *, query: Query | None = None, timeout: float | None = None
1580
1580
  ) -> JSONValue:
1581
- """GET /v1/sessions/:sid/fs/file — read one workspace file as text."""
1581
+ """GET /v1/sessions/:sid/fs/file — preview one workspace file by its kind."""
1582
1582
  response = self._request(
1583
1583
  "GET",
1584
1584
  "/v1/sessions/:sid/fs/file",
@@ -52,6 +52,7 @@ CHECK_KEYS = frozenset(
52
52
  "id",
53
53
  "max_form_output_chars",
54
54
  "max_patch_calls_per_file",
55
+ "max_patch_refusals",
55
56
  "max_total_output_chars",
56
57
  "measurement",
57
58
  "verify",
@@ -306,51 +307,27 @@ def council_recovery_evidence(form_outputs, activities, expected):
306
307
  block.get("text") for block in blocks if block.get("type") == "markdown"
307
308
  ]
308
309
 
309
- publications = [
310
- row
311
- for row in activities
312
- if row.get("operation") == "council.publish" and row.get("state") == "succeeded"
313
- ]
314
- roots = [
315
- row
316
- for row in publications
317
- if references(row, "council-entry") == {thread_id}
318
- and references(row, "council-thread") == {thread_id}
319
- and bodies(row) == [expected["root_content"]]
320
- ]
321
- replies = [
322
- row
323
- for row in publications
324
- if references(row, "council-entry") == {entry_id}
325
- and references(row, "council-thread") == {thread_id}
326
- and bodies(row) == [expected["reply_content"]]
327
- ]
310
+ # A publication has no Activity row. The repaired read proves both messages.
328
311
  reads = [
329
312
  row
330
313
  for row in activities
331
314
  if row.get("operation") == "council.read" and row.get("state") == "succeeded"
332
315
  ]
333
- if (
334
- len(publications) != 2
335
- or len(roots) != 1
336
- or len(replies) != 1
337
- or len(reads) != 1
338
- ):
316
+ if len(reads) != 1:
339
317
  return (
340
- ["Council recovery lacks the two real publications and one repaired read"],
318
+ ["Council recovery lacks one repaired read"],
341
319
  set(),
342
320
  [],
343
321
  )
344
- root, reply, repaired = roots[0], replies[0], reads[0]
322
+ repaired = reads[0]
345
323
  failures = []
346
324
  if failed.get("state") != "failed" or failed.get("scope") != event.get("scope"):
347
325
  failures.append("Council recovery error does not match its failed Activity")
348
326
  if any(
349
- not row.get("scope") or row["scope"] not in order
350
- for row in (root, reply, failed, repaired)
327
+ not row.get("scope") or row["scope"] not in order for row in (failed, repaired)
351
328
  ):
352
329
  failures.append("Council recovery is missing a form scope")
353
- if not position(root) < position(reply) < position(failed) < position(repaired):
330
+ if not position(failed) < position(repaired):
354
331
  failures.append(
355
332
  "Council recovery did not read the thread after the reply-ID error"
356
333
  )
@@ -436,6 +413,18 @@ def structured_failures(sc, work, answer, activities):
436
413
  failures.append(
437
414
  f"{path} was patched {count} times; collect its edits into at most {limit} patch call(s)"
438
415
  )
416
+ # User report 2026-10-31: each refused patch cost the agent one more step.
417
+ limit = sc.get("max_patch_refusals")
418
+ if type(limit) is int:
419
+ refused = sum(
420
+ 1
421
+ for row in activities
422
+ if row["operation"] == "patch"
423
+ for call in row.get("children") or [row]
424
+ if call.get("state", row.get("state")) == "failed"
425
+ )
426
+ if refused > limit:
427
+ failures.append(f"patch was refused {refused} times; the limit is {limit}")
439
428
  forbidden = set(sc.get("forbid_tools") or [])
440
429
  for operation in sorted({row["operation"] for row in activities} & forbidden):
441
430
  failures.append(f"forbidden tool {operation!r} was used")
@@ -3886,6 +3886,30 @@ def notify(text, level="info"):
3886
3886
  _host.notify(str(text), str(level))
3887
3887
 
3888
3888
 
3889
+ def write_private_file(path: str | PathLike[str], text: str) -> None:
3890
+ """Replace `path` with `text` in one atomic step, readable only by its owner.
3891
+
3892
+ A failure keeps the previous file. A symbolic link writes the file it names.
3893
+ Missing parent directories are created owner-only.
3894
+ """
3895
+ target = Path(path)
3896
+ if target.is_symlink():
3897
+ target = target.resolve(strict=True)
3898
+ for directory in reversed([d for d in target.parents if not d.exists()]):
3899
+ directory.mkdir(mode=0o700)
3900
+ tmp = target.with_name(f".{target.name}.tmp-{os.urandom(8).hex()}")
3901
+ fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
3902
+ try:
3903
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
3904
+ handle.write(text)
3905
+ handle.flush()
3906
+ os.fsync(handle.fileno())
3907
+ os.replace(tmp, target)
3908
+ except BaseException:
3909
+ tmp.unlink(missing_ok=True)
3910
+ raise
3911
+
3912
+
3889
3913
  def _shell_options(name, opts):
3890
3914
  if not isinstance(opts, dict):
3891
3915
  raise TypeError(
@@ -6277,6 +6301,12 @@ def live(title, nodes, placement=None, key=None, on_event=None, **options):
6277
6301
 
6278
6302
  # -- Extension live-view test harness -----------------------------------------
6279
6303
 
6304
+ # The refusal of a live group without an id, in the recorder and the outside host.
6305
+ _LIVE_GROUP_NEEDS_ID = (
6306
+ "a live view group needs an id, because patches and after= name it: "
6307
+ "write vis.ui.group('actions', ...) or vis.ui.wrap('actions', ...)"
6308
+ )
6309
+
6280
6310
 
6281
6311
  class _LiveRecorder:
6282
6312
  """An isolated in-memory live host for testing any extension live view.
@@ -6366,6 +6396,12 @@ class _LiveRecorder:
6366
6396
  node.setdefault("variant", "braille")
6367
6397
  node.setdefault("is_active", True)
6368
6398
  elif kind == "button":
6399
+ from blockether.vis import _contracts
6400
+
6401
+ complaint = _contracts.button_variant_error(node.get("variant"))
6402
+ if complaint:
6403
+ # Vis raises VisToolError, a RuntimeError, so the recorder does too.
6404
+ raise RuntimeError(f"button {node.get('id')}: {complaint}")
6369
6405
  node["clicks"] = 0
6370
6406
  node.setdefault("is_disabled", False)
6371
6407
  elif kind == "card" and node.get("is_selectable"):
@@ -6568,12 +6604,9 @@ class _LiveRecorder:
6568
6604
  if action == "open":
6569
6605
  for node in self._nodes(envelope["view"].get("nodes")):
6570
6606
  # The engine refuses a live group without an id, so a test must too.
6607
+ # Vis raises VisToolError, a RuntimeError, so the recorder does too.
6571
6608
  if node.get("type") == "group" and not node.get("id"):
6572
- raise AssertionError(
6573
- "a live view group needs an id, because patches and "
6574
- "after= name it: write vis.ui.group('actions', ...) "
6575
- "or vis.ui.wrap('actions', ...)"
6576
- )
6609
+ raise RuntimeError(_LIVE_GROUP_NEEDS_ID)
6577
6610
  self._view = self._materialize(envelope["view"])
6578
6611
  self._declared = set(envelope.get("validators") or [])
6579
6612
  self._verdicts = {}
@@ -7077,14 +7110,22 @@ def _ui_spinner(node_id, text="Working", *, variant="braille", is_active=True, *
7077
7110
  )
7078
7111
 
7079
7112
 
7080
- def _ui_button(node_id, label, *, is_disabled=False, **spec):
7113
+ def _ui_button(node_id, label, *, is_disabled=False, variant=None, **spec):
7081
7114
  """An operator action. Accepted presses increment ``clicks`` in ``view.state()``.
7082
7115
 
7083
- No callback or code crosses the wire. The producer decides how to respond.
7116
+ ``variant`` is ``"primary"``, ``"secondary"`` (the default) or ``"destructive"``.
7117
+ Give one primary action per group. No callback or code crosses the wire.
7084
7118
  Disabled buttons and completed receipts cannot be activated.
7085
7119
  """
7120
+ from blockether.vis import _contracts
7121
+
7122
+ complaint = _contracts.button_variant_error(variant)
7123
+ if complaint:
7124
+ raise ValueError(f"button {node_id}: {complaint}")
7086
7125
  return _live_node(
7087
- "button", node_id, dict(spec, label=str(label), is_disabled=is_disabled)
7126
+ "button",
7127
+ node_id,
7128
+ dict(spec, label=str(label), is_disabled=is_disabled, variant=variant),
7088
7129
  )
7089
7130
 
7090
7131
 
@@ -43,6 +43,8 @@ LATEST = "latest"
43
43
  LOCK_WAIT_S = 300
44
44
  MAX_METADATA = 128 * 1024
45
45
  _EXCLUDED = {".git", ".venv", "venv", "__pycache__", "node_modules", ".DS_Store"}
46
+ # `.env*` holds secrets. `.venv-*` is an environment that Vis prepares for one interpreter.
47
+ _EXCLUDED_PREFIXES = (".env", ".venv-")
46
48
 
47
49
 
48
50
  def _relative(value):
@@ -51,7 +53,7 @@ def _relative(value):
51
53
  if len(value) > 512 or any(ord(c) < 32 or ord(c) == 127 for c in value):
52
54
  raise ValueError("Project paths are too long or contain control characters")
53
55
  if any(
54
- p in {"", ".", ".."} or p in _EXCLUDED or p.startswith(".env")
56
+ p in {"", ".", ".."} or p in _EXCLUDED or p.startswith(_EXCLUDED_PREFIXES)
55
57
  for p in value.split("/")
56
58
  ):
57
59
  raise ValueError("Project contains an unsafe or excluded path")
@@ -346,7 +348,11 @@ def _checkout(repository, directory, revision):
346
348
 
347
349
  def _copy_project(source, destination):
348
350
  def excluded(_directory, names):
349
- return {name for name in names if name in _EXCLUDED or name.startswith(".env")}
351
+ return {
352
+ name
353
+ for name in names
354
+ if name in _EXCLUDED or name.startswith(_EXCLUDED_PREFIXES)
355
+ }
350
356
 
351
357
  count, size = 0, 0
352
358
  for base, directories, files in os.walk(source):
@@ -48,16 +48,22 @@ def sdk_flow(mode):
48
48
 
49
49
  def sdk_native_wait(marker: str):
50
50
  "Wait inside C until cancellation reclaims this trusted extension worker."
51
+ import _thread
51
52
  import os
52
53
  import threading
53
54
  from pathlib import Path
54
- condition = threading.Condition()
55
+ # A raw `_thread` lock acquire stays in C. The runtime slices threading's
56
+ # own waits, so a Condition wait would unwind on the interrupt.
57
+ gate = threading.Lock()
58
+ waiter = _thread.allocate_lock()
59
+ waiter.acquire()
55
60
  def notify_waiting():
56
- with condition:
61
+ with gate:
57
62
  Path(marker).write_text(str(os.getpid()))
58
- with condition:
59
- threading.Thread(target=notify_waiting, daemon=True).start()
60
- condition.wait(120)
63
+ gate.acquire()
64
+ threading.Thread(target=notify_waiting, daemon=True).start()
65
+ gate.release()
66
+ waiter.acquire(timeout=120)
61
67
 
62
68
  vis.register_extension(vis.Extension(name="sdk-fixture", description="SDK integration fixture", alias="sdk",
63
69
  symbols=[vis.Symbol(sdk_flow, activity=vis.Activity(presenter="observation", label="SDK flow", render=sdk_activity)),
@@ -602,9 +608,9 @@ def test_real_council_roundtrip(tmp_path, monkeypatch, transport):
602
608
  "turn.cancelled",
603
609
  }:
604
610
  break
611
+ # User request: the team notes show each message, so a publication has no row.
605
612
  assert sorted(operations.values()) == [
606
613
  "council.get",
607
- "council.publish",
608
614
  "council.threads",
609
615
  ]
610
616
  assert turn.wait(timeout=30)["status"] == "completed"
@@ -1444,6 +1444,22 @@ class EvaluationIntegrationEdgeTest(unittest.TestCase):
1444
1444
  group = atif.row("group-call-1", "patch", children=split)
1445
1445
  self.assertEqual(1, len(run(group)))
1446
1446
 
1447
+ def test_patch_refusals_stay_within_the_limit(self):
1448
+ # User report 2026-10-31: each refused patch cost the agent one more step.
1449
+ def run(*rows):
1450
+ return grade.structured_failures(
1451
+ {"max_patch_refusals": 0}, ".", "", list(rows)
1452
+ )
1453
+
1454
+ written = atif.row("call-1", "patch", summary="app.py")
1455
+ refused = atif.row("call-2", "patch", "failed", summary="app.py", sequence=2)
1456
+ self.assertEqual([], run(written))
1457
+ self.assertEqual(
1458
+ ["patch was refused 1 times; the limit is 0"], run(written, refused)
1459
+ )
1460
+ group = atif.row("group-call-1", "patch", children=[written, refused])
1461
+ self.assertEqual(1, len(run(group)))
1462
+
1447
1463
  def test_malformed_run_metrics_are_a_failure_not_a_crash(self):
1448
1464
  helper = DiscoveryEvaluationTest()
1449
1465
  malformed = {
@@ -1509,17 +1525,6 @@ class CouncilRecoveryTest(unittest.TestCase):
1509
1525
  )
1510
1526
 
1511
1527
  return {
1512
- "root": row(
1513
- "root", "council.publish", "succeeded", [42], expected["root_content"]
1514
- ),
1515
- "reply": row(
1516
- "reply",
1517
- "council.publish",
1518
- "succeeded",
1519
- [43],
1520
- expected["reply_content"],
1521
- sequence=2,
1522
- ),
1523
1528
  "mistake": row("mistake", "council.read", "failed", []),
1524
1529
  "error": {
1525
1530
  "type": "error",
@@ -1531,11 +1536,12 @@ class CouncilRecoveryTest(unittest.TestCase):
1531
1536
  }
1532
1537
 
1533
1538
  def blocks(self, parts):
1534
- """Return the publish, mistake and repair blocks of the run, in that order."""
1539
+ """Return the publish, mistake and repair blocks of the run, in that order.
1540
+
1541
+ A Council publication leaves no Activity row, so the publish block has none.
1542
+ """
1535
1543
  return [
1536
- atif.block(
1537
- "t1/i1/f1", activity=atif.receipt(parts["root"], parts["reply"])
1538
- ),
1544
+ atif.block("t1/i1/f1"),
1539
1545
  atif.block(
1540
1546
  "t1/i2/f1",
1541
1547
  error=parts["error"],
@@ -241,16 +241,44 @@ def test_table_builder_refuses_text_columns():
241
241
  vis.ui.table("chats", columns=[vis.ui.table_column("chat", "Chat"), "Category"])
242
242
 
243
243
 
244
+ def test_button_variant_is_kept_and_a_typo_names_the_variants():
245
+ # #430: a button had no hierarchy, and a misspelled variant reached the host.
246
+ assert vis.ui.button("save", "Save", variant="primary")["variant"] == "primary"
247
+ assert "variant" not in vis.ui.button("save", "Save")
248
+ with pytest.raises(ValueError, match=r"primary, secondary, destructive.*'danger'"):
249
+ vis.ui.button("drop", "Drop", variant="danger")
250
+
251
+
252
+ def test_outside_refuses_an_unknown_button_variant():
253
+ # #430: outside Vis a raw button with a typo variant was accepted.
254
+ button = {"id": "drop", "type": "button", "label": "Drop", "variant": "danger"}
255
+ view = {"title": "Rows", "nodes": [button]}
256
+ with pytest.raises(RuntimeError, match=r"primary, secondary, destructive"):
257
+ _outside.live(json.dumps({"op": "open", "view": view}))
258
+
259
+
244
260
  def test_recorder_refuses_a_live_group_without_an_id(monkeypatch):
245
261
  # #428: the recorder hid the buttons of an unnamed group; the engine refuses it.
262
+ # Vis raises VisToolError, a RuntimeError, so the recorder must raise one too.
246
263
  monkeypatch.setattr(vis, "_host", vis.testing.LiveRecorder(vis._host))
247
- with pytest.raises(AssertionError, match="group needs an id"):
264
+ with pytest.raises(
265
+ RuntimeError, match=r"group needs an id.*vis\.ui\.group\('actions'"
266
+ ):
248
267
  with vis.live(
249
268
  "Chats", [vis.ui.wrap(vis.ui.button("save", "Save"))], flush_ms=0
250
269
  ):
251
270
  pass
252
271
 
253
272
 
273
+ def test_outside_refuses_a_live_group_without_an_id_with_the_fix():
274
+ # #428: outside Vis the refusal named no fix for an unnamed group.
275
+ view = {"title": "Chats", "nodes": [vis.ui.wrap(vis.ui.button("save", "Save"))]}
276
+ with pytest.raises(
277
+ RuntimeError, match=r"group needs an id.*vis\.ui\.group\('actions'"
278
+ ):
279
+ _outside.live(json.dumps({"op": "open", "view": view}))
280
+
281
+
254
282
  def test_outside_refused_patch_leaves_state_unchanged():
255
283
  opened = json.loads(
256
284
  _outside.live(