vis-agent 0.2.38.dev7570__tar.gz → 0.2.39.dev7588__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.
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/PKG-INFO +1 -1
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/gateway.json +4 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/toggle.json +3 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/pyproject.toml +1 -1
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/_outside.py +111 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/extension.py +213 -1
- vis_agent-0.2.39.dev7588/tests/test_attachments.py +109 -0
- vis_agent-0.2.39.dev7588/tests/test_read_session.py +55 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_scoped_settings.py +17 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/.gitignore +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/LICENSE +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/README.md +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-arguments.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-cases.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-copy.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-execution.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-groups.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-read-session.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-reads.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-results.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-tables.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/diff.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/live-primitives.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/session-health.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/view.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/activity.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/agents.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/automations.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/common.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/config.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/content.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/council.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/diff.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/improve.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/provider.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/rooms.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/symbol.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/schema/view.json +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/examples/greeter/tests/test_greeter.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/hatch_build.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/__init__.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/_contracts.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/_wire.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/activity.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/engine/__init__.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/engine/_agent.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/engine/_agents.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/engine/_client.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/engine/_council.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/engine/_extensions.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/engine/_local.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/evals/__init__.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/evals/harbor.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/evals/stats.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/evals/trajectory.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/extension_package.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/py.typed +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/rooms.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/views.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/conftest.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_activity.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_agent.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_agent_extension_lifecycle.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_agent_extensions.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_agents.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_api_guides.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_authoring.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_catalog.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_catalog_recipe.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_ci_report_recipe.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_client.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_client_extensions_engine.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_code_quality_recipe.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_contracts.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_council.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_declarations.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_engine.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_engine_docstrings.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_evals_harbor.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_evals_stats.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_evals_trajectory.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_extension_package.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_extension_reference.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_gateway_performance.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_goals.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_live_primitives.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_live_wait.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_local.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_method_typing.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_monitor_recipe.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_namespace.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_outside.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_providers.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_record_arguments.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_registration.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_repair_hooks.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_result_sequence.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_rooms.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_sdk_bootstrap.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_sdk_guide.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_sdk_typing.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_structured_live.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_symbol_contract.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_testing.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_text_list_arguments.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_tools.py +0 -0
- {vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vis-agent
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.39.dev7588
|
|
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
|
|
@@ -2732,6 +2732,10 @@
|
|
|
2732
2732
|
"type": "string"
|
|
2733
2733
|
}
|
|
2734
2734
|
},
|
|
2735
|
+
"change_error": {
|
|
2736
|
+
"type": "string",
|
|
2737
|
+
"description": "Present after a write when the owner's change hook failed. The value stays saved."
|
|
2738
|
+
},
|
|
2735
2739
|
"parent": {
|
|
2736
2740
|
"type": "string",
|
|
2737
2741
|
"description": "Id of the setting that this row belongs under."
|
|
@@ -100,6 +100,9 @@
|
|
|
100
100
|
"minItems": 1
|
|
101
101
|
},
|
|
102
102
|
"visible_fn": {},
|
|
103
|
+
"on_change_fn": {
|
|
104
|
+
"description": "Local callback that receives the id, old value, new value and scope once after a write changes the effective value."
|
|
105
|
+
},
|
|
103
106
|
"scopes": {
|
|
104
107
|
"type": "array",
|
|
105
108
|
"items": {
|
|
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "vis-agent"
|
|
11
|
-
version = "0.2.
|
|
11
|
+
version = "0.2.39.dev7588"
|
|
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"
|
|
@@ -8,6 +8,7 @@ primes prompt values. With `VIS_OUTSIDE_NONINTERACTIVE=1`, prompts return
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import base64
|
|
11
12
|
import json
|
|
12
13
|
import os
|
|
13
14
|
import secrets
|
|
@@ -81,6 +82,22 @@ def call_tool(tool, args, kwargs):
|
|
|
81
82
|
)
|
|
82
83
|
|
|
83
84
|
|
|
85
|
+
def read_session(target):
|
|
86
|
+
"""Refuse a session read without a bound Vis session and its database."""
|
|
87
|
+
raise Refused(
|
|
88
|
+
"vis.read_session needs a bound Vis session. Outside an extension, read the "
|
|
89
|
+
"session with the SDK client's get_session_transcript(session_id)."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def list_sessions(search):
|
|
94
|
+
"""Refuse a session search without a bound Vis session and its database."""
|
|
95
|
+
raise Refused(
|
|
96
|
+
"vis.list_sessions needs a bound Vis session. Outside an extension, list the "
|
|
97
|
+
"sessions with the SDK client's list_sessions()."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
84
101
|
# -- Where the outside host keeps things --------------------------------------
|
|
85
102
|
|
|
86
103
|
|
|
@@ -212,6 +229,93 @@ def forget_secret(handle):
|
|
|
212
229
|
return _VAULT.pop(str(handle), None) is not None
|
|
213
230
|
|
|
214
231
|
|
|
232
|
+
# -- Attachments ----------------------------------------------------------------
|
|
233
|
+
# Outside Vis, no session stores artifacts. They stay in this process memory, so
|
|
234
|
+
# an extension test can attach an artifact and read it back.
|
|
235
|
+
|
|
236
|
+
_ARTIFACTS = []
|
|
237
|
+
_ARTIFACT_BYTES = {}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def attach(options):
|
|
241
|
+
"""Store one artifact in memory and return its descriptor as JSON."""
|
|
242
|
+
data = base64.b64decode(options.get("base64") or "")
|
|
243
|
+
if not data:
|
|
244
|
+
raise ValueError("vis.attach: empty payload.")
|
|
245
|
+
name = options.get("filename") or "artifact"
|
|
246
|
+
media_type = (
|
|
247
|
+
options.get("media_type")
|
|
248
|
+
or options.get("media_type_hint")
|
|
249
|
+
or "application/octet-stream"
|
|
250
|
+
)
|
|
251
|
+
with _LOCK:
|
|
252
|
+
row = {
|
|
253
|
+
"id": uuid.uuid4().hex,
|
|
254
|
+
"source": "tool",
|
|
255
|
+
"filename": name,
|
|
256
|
+
"version": 1 + sum(1 for r in _ARTIFACTS if r["filename"] == name),
|
|
257
|
+
"media_type": media_type,
|
|
258
|
+
"kind": options.get("kind")
|
|
259
|
+
or ("image" if media_type.startswith("image/") else "file"),
|
|
260
|
+
"size": len(data),
|
|
261
|
+
"is_pending": True,
|
|
262
|
+
"audience": options.get("audience") or "both",
|
|
263
|
+
"commentable": options.get("commentable") is True,
|
|
264
|
+
}
|
|
265
|
+
_ARTIFACTS.append(row)
|
|
266
|
+
_ARTIFACT_BYTES[row["id"]] = data
|
|
267
|
+
return json.dumps(row)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def list_attachments():
|
|
271
|
+
"""List the in-memory artifact descriptors as JSON, oldest first."""
|
|
272
|
+
with _LOCK:
|
|
273
|
+
return json.dumps(_ARTIFACTS)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _artifact(caller, target, version):
|
|
277
|
+
# The same addressing as the engine: an id, or a filename with an optional
|
|
278
|
+
# version, where a negative version counts back from the latest one.
|
|
279
|
+
with _LOCK:
|
|
280
|
+
if version is None:
|
|
281
|
+
for row in _ARTIFACTS:
|
|
282
|
+
if row["id"] == target:
|
|
283
|
+
return row
|
|
284
|
+
versions = sorted(
|
|
285
|
+
(r for r in _ARTIFACTS if r["filename"] == target),
|
|
286
|
+
key=lambda r: r["version"],
|
|
287
|
+
)
|
|
288
|
+
if versions:
|
|
289
|
+
if version is None:
|
|
290
|
+
return versions[-1]
|
|
291
|
+
if -len(versions) <= version <= -1:
|
|
292
|
+
return versions[version]
|
|
293
|
+
for row in versions:
|
|
294
|
+
if row["version"] == version:
|
|
295
|
+
return row
|
|
296
|
+
raise LookupError(f"{caller}: {target!r} has no version {version}.")
|
|
297
|
+
raise LookupError(f"{caller}: unknown id or filename {target!r}.")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def get_attachment(target, version):
|
|
301
|
+
"""Return one in-memory artifact descriptor as JSON."""
|
|
302
|
+
return json.dumps(_artifact("get_attachment", target, version))
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def read_attachment(target, version):
|
|
306
|
+
"""Return the bytes of one in-memory artifact as base64 text."""
|
|
307
|
+
row = _artifact("read_attachment", target, version)
|
|
308
|
+
return base64.b64encode(_ARTIFACT_BYTES[row["id"]]).decode("ascii")
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def show_attachment(target, version):
|
|
312
|
+
"""Accept an image. Outside Vis, no model request follows to show it to."""
|
|
313
|
+
row = _artifact("show_attachment", target, version)
|
|
314
|
+
if not row["media_type"].startswith("image/"):
|
|
315
|
+
raise ValueError(f"show_attachment: {row['filename']!r} is not an image.")
|
|
316
|
+
return [row["id"], row["filename"], row["media_type"], row["size"]]
|
|
317
|
+
|
|
318
|
+
|
|
215
319
|
# -- Environment --------------------------------------------------------------
|
|
216
320
|
|
|
217
321
|
|
|
@@ -1332,12 +1436,19 @@ _IMPLEMENTATIONS = {
|
|
|
1332
1436
|
"jailed_shell": jailed_shell,
|
|
1333
1437
|
"council_wake": council_wake,
|
|
1334
1438
|
"call_tool": call_tool,
|
|
1439
|
+
"read_session": read_session,
|
|
1440
|
+
"list_sessions": list_sessions,
|
|
1335
1441
|
"request_input": request_input,
|
|
1336
1442
|
"live": live,
|
|
1337
1443
|
"activity": lambda blocks: False,
|
|
1338
1444
|
"reveal_secret": reveal_secret,
|
|
1339
1445
|
"forget_secret": forget_secret,
|
|
1340
1446
|
"declare_env": declare_env,
|
|
1447
|
+
"attach": attach,
|
|
1448
|
+
"list_attachments": list_attachments,
|
|
1449
|
+
"get_attachment": get_attachment,
|
|
1450
|
+
"read_attachment": read_attachment,
|
|
1451
|
+
"show_attachment": show_attachment,
|
|
1341
1452
|
}
|
|
1342
1453
|
|
|
1343
1454
|
|
|
@@ -9,10 +9,14 @@ from __future__ import annotations
|
|
|
9
9
|
import __future__
|
|
10
10
|
|
|
11
11
|
import ast
|
|
12
|
+
import base64
|
|
12
13
|
import builtins
|
|
13
14
|
import inspect
|
|
15
|
+
import io
|
|
14
16
|
import json
|
|
15
17
|
import math
|
|
18
|
+
import mimetypes
|
|
19
|
+
import os
|
|
16
20
|
import sys
|
|
17
21
|
import time
|
|
18
22
|
from collections.abc import Callable, Mapping, Sequence
|
|
@@ -138,6 +142,34 @@ class Host(Protocol):
|
|
|
138
142
|
"""Run one active session tool by the name `python_execution` calls."""
|
|
139
143
|
...
|
|
140
144
|
|
|
145
|
+
def read_session(self, target: str | None) -> str:
|
|
146
|
+
"""Read one whole session as the sandbox `read_session` does. Return JSON."""
|
|
147
|
+
...
|
|
148
|
+
|
|
149
|
+
def list_sessions(self, search: str | None) -> str:
|
|
150
|
+
"""List sessions as the sandbox `list_sessions` does. Return JSON."""
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
def attach(self, options: Mapping[str, Any]) -> str:
|
|
154
|
+
"""Store one artifact of the running block. Return its descriptor as JSON."""
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
def list_attachments(self) -> str:
|
|
158
|
+
"""List the session's artifact descriptors as JSON, pending ones included."""
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
def get_attachment(self, target: str, version: int | None) -> str:
|
|
162
|
+
"""Return the descriptor of one artifact as JSON."""
|
|
163
|
+
...
|
|
164
|
+
|
|
165
|
+
def read_attachment(self, target: str, version: int | None) -> str:
|
|
166
|
+
"""Return the bytes of one artifact as base64 text."""
|
|
167
|
+
...
|
|
168
|
+
|
|
169
|
+
def show_attachment(self, target: str, version: int | None) -> Sequence[Any]:
|
|
170
|
+
"""Queue a stored image for the next model request: id, filename, type, size."""
|
|
171
|
+
...
|
|
172
|
+
|
|
141
173
|
|
|
142
174
|
# The engine seeds `_host` into the module dict before exec.
|
|
143
175
|
_host: Host
|
|
@@ -1007,6 +1039,9 @@ class Setting(Generic[_SettingValue]):
|
|
|
1007
1039
|
for a choice setting. Outside Vis, it returns the declared default.
|
|
1008
1040
|
`parent` names another setting of the same section. Settings shows this setting
|
|
1009
1041
|
under that setting. Settings can nest at any depth.
|
|
1042
|
+
`on_change(old, new, scope)` runs once after a person changes the setting in the
|
|
1043
|
+
app or TUI and its effective value changes. `scope` is `global`, `project`, `group`
|
|
1044
|
+
or `session`. Vis keeps the saved value if the hook raises, and shows the error.
|
|
1010
1045
|
"""
|
|
1011
1046
|
|
|
1012
1047
|
id: str
|
|
@@ -1017,6 +1052,7 @@ class Setting(Generic[_SettingValue]):
|
|
|
1017
1052
|
scopes: Sequence[str] | None = None
|
|
1018
1053
|
description: str | None = None
|
|
1019
1054
|
parent: str | None = None
|
|
1055
|
+
on_change: Callable[[Any, Any, str], Any] | None = None
|
|
1020
1056
|
|
|
1021
1057
|
def __post_init__(self):
|
|
1022
1058
|
for name, value in (("scopes", self.scopes), ("choices", self.choices)):
|
|
@@ -1025,7 +1061,11 @@ class Setting(Generic[_SettingValue]):
|
|
|
1025
1061
|
if not isinstance(value, (list, tuple)):
|
|
1026
1062
|
raise TypeError(f"Setting {name} must be a list or tuple")
|
|
1027
1063
|
object.__setattr__(self, name, tuple(value))
|
|
1028
|
-
|
|
1064
|
+
if self.on_change is not None and not callable(self.on_change):
|
|
1065
|
+
raise TypeError("Setting on_change must be callable")
|
|
1066
|
+
declaration = self._spec()
|
|
1067
|
+
declaration.pop("on_change", None)
|
|
1068
|
+
spec = _host.setting_declaration(declaration)
|
|
1029
1069
|
object.__setattr__(self, "scopes", tuple(spec["scopes"]))
|
|
1030
1070
|
if self.type == "boolean" and type(self.default) is not bool:
|
|
1031
1071
|
raise ValueError("A boolean setting requires a boolean default")
|
|
@@ -1054,6 +1094,8 @@ class Setting(Generic[_SettingValue]):
|
|
|
1054
1094
|
spec["description"] = self.description
|
|
1055
1095
|
if self.parent is not None:
|
|
1056
1096
|
spec["parent"] = self.parent
|
|
1097
|
+
if self.on_change is not None:
|
|
1098
|
+
spec["on_change"] = self.on_change
|
|
1057
1099
|
return spec
|
|
1058
1100
|
|
|
1059
1101
|
def value(self) -> _SettingValue:
|
|
@@ -3796,6 +3838,176 @@ def forget(handle):
|
|
|
3796
3838
|
return bool(_host.forget_secret(str(handle)))
|
|
3797
3839
|
|
|
3798
3840
|
|
|
3841
|
+
# -- Attachments ----------------------------------------------------------------
|
|
3842
|
+
# An extension tool stores and reads artifacts like the sandbox `attach` verbs.
|
|
3843
|
+
# The engine binds each call to the python_execution block that runs the tool.
|
|
3844
|
+
|
|
3845
|
+
_ATTACH_AUDIENCES = ("both", "user", "model")
|
|
3846
|
+
|
|
3847
|
+
# Pillow encoders by filename suffix, as in the sandbox `attach` verb.
|
|
3848
|
+
_PIL_FORMATS = {
|
|
3849
|
+
".png": "PNG",
|
|
3850
|
+
".jpg": "JPEG",
|
|
3851
|
+
".jpeg": "JPEG",
|
|
3852
|
+
".gif": "GIF",
|
|
3853
|
+
".bmp": "BMP",
|
|
3854
|
+
".webp": "WEBP",
|
|
3855
|
+
".tif": "TIFF",
|
|
3856
|
+
".tiff": "TIFF",
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
|
|
3860
|
+
def _attachment_row(row):
|
|
3861
|
+
# The engine sends kebab-case keys; the SDK returns snake_case keys.
|
|
3862
|
+
return {str(k).replace("-", "_"): v for k, v in dict(row).items()}
|
|
3863
|
+
|
|
3864
|
+
|
|
3865
|
+
def _attachment_source(source, filename):
|
|
3866
|
+
"""Return `(bytes, filename)` for a path, bytes, a figure or a Pillow image."""
|
|
3867
|
+
if isinstance(source, (bytes, bytearray, memoryview)):
|
|
3868
|
+
return bytes(source), str(filename) if filename else "artifact"
|
|
3869
|
+
if hasattr(source, "savefig"):
|
|
3870
|
+
buffer = io.BytesIO()
|
|
3871
|
+
source.savefig(buffer, format="png")
|
|
3872
|
+
return buffer.getvalue(), str(filename) if filename else "figure.png"
|
|
3873
|
+
if hasattr(source, "save") and hasattr(source, "mode") and hasattr(source, "size"):
|
|
3874
|
+
name = str(filename) if filename else "image.png"
|
|
3875
|
+
buffer = io.BytesIO()
|
|
3876
|
+
source.save(buffer, format=_PIL_FORMATS.get(Path(name).suffix.lower(), "PNG"))
|
|
3877
|
+
return buffer.getvalue(), name
|
|
3878
|
+
if not isinstance(source, (str, PathLike)):
|
|
3879
|
+
raise TypeError(
|
|
3880
|
+
"vis.attach: source must be a path, bytes, an image or a figure object, got "
|
|
3881
|
+
+ type(source).__name__
|
|
3882
|
+
)
|
|
3883
|
+
path = Path(os.path.expandvars(os.path.expanduser(os.fspath(source))))
|
|
3884
|
+
if path.is_dir():
|
|
3885
|
+
raise IsADirectoryError(f"vis.attach: {path} is a directory - attach one file")
|
|
3886
|
+
try:
|
|
3887
|
+
data = path.read_bytes()
|
|
3888
|
+
except FileNotFoundError:
|
|
3889
|
+
raise FileNotFoundError(f"vis.attach: no such file: {path}") from None
|
|
3890
|
+
return data, str(filename) if filename else (path.name or "artifact")
|
|
3891
|
+
|
|
3892
|
+
|
|
3893
|
+
def _attachment_target(target, version):
|
|
3894
|
+
if isinstance(target, Mapping):
|
|
3895
|
+
target = target.get("id") or target.get("filename") or ""
|
|
3896
|
+
if version is not None and (type(version) is not int):
|
|
3897
|
+
raise TypeError("attachment version must be an integer")
|
|
3898
|
+
return str(target), version
|
|
3899
|
+
|
|
3900
|
+
|
|
3901
|
+
def attach(
|
|
3902
|
+
source,
|
|
3903
|
+
filename=None,
|
|
3904
|
+
*,
|
|
3905
|
+
kind=None,
|
|
3906
|
+
media_type=None,
|
|
3907
|
+
label=None,
|
|
3908
|
+
audience="both",
|
|
3909
|
+
commentable=False,
|
|
3910
|
+
):
|
|
3911
|
+
"""Store an artifact as a durable attachment of the running block.
|
|
3912
|
+
|
|
3913
|
+
`source` is a path, bytes, a Matplotlib figure or a Pillow image. The engine
|
|
3914
|
+
stores the artifact with the `python_execution` block that called this tool,
|
|
3915
|
+
exactly like the sandbox `attach` verb. A second attachment under the same
|
|
3916
|
+
`filename` is the next version of that artifact. `audience` is `both`, `user`
|
|
3917
|
+
or `model`; a PDF or HTML document is for the user only. Set
|
|
3918
|
+
`commentable=True` to let a person comment on this version.
|
|
3919
|
+
|
|
3920
|
+
Returns the descriptor dict: id, filename, version, media_type, kind, size,
|
|
3921
|
+
audience and commentable. Every read function takes it as its target.
|
|
3922
|
+
"""
|
|
3923
|
+
data, name = _attachment_source(source, filename)
|
|
3924
|
+
if audience not in _ATTACH_AUDIENCES:
|
|
3925
|
+
raise ValueError(
|
|
3926
|
+
f"vis.attach: audience must be 'both', 'user' or 'model', got {audience!r}"
|
|
3927
|
+
)
|
|
3928
|
+
if not isinstance(commentable, bool):
|
|
3929
|
+
raise TypeError("vis.attach: commentable must be a boolean")
|
|
3930
|
+
if not data:
|
|
3931
|
+
raise ValueError("vis.attach: empty payload.")
|
|
3932
|
+
options = {
|
|
3933
|
+
"base64": base64.b64encode(data).decode("ascii"),
|
|
3934
|
+
"filename": name,
|
|
3935
|
+
"media_type": media_type,
|
|
3936
|
+
"media_type_hint": mimetypes.guess_type(name)[0],
|
|
3937
|
+
"kind": kind,
|
|
3938
|
+
"audience": audience,
|
|
3939
|
+
"label": label,
|
|
3940
|
+
"commentable": commentable,
|
|
3941
|
+
}
|
|
3942
|
+
payload = _host.attach({k: v for k, v in options.items() if v is not None})
|
|
3943
|
+
return _attachment_row(json.loads(payload))
|
|
3944
|
+
|
|
3945
|
+
|
|
3946
|
+
def list_attachments(name=None):
|
|
3947
|
+
"""List the artifact descriptors of this session, oldest first.
|
|
3948
|
+
|
|
3949
|
+
The list includes the artifacts that the running block attached. With `name`,
|
|
3950
|
+
it lists the versions of that one artifact.
|
|
3951
|
+
"""
|
|
3952
|
+
rows = [_attachment_row(row) for row in json.loads(_host.list_attachments())]
|
|
3953
|
+
if name is None:
|
|
3954
|
+
return rows
|
|
3955
|
+
versions = [row for row in rows if row.get("filename") == str(name)]
|
|
3956
|
+
return sorted(versions, key=lambda row: int(row.get("version") or 1))
|
|
3957
|
+
|
|
3958
|
+
|
|
3959
|
+
def get_attachment(target, version=None):
|
|
3960
|
+
"""Return the descriptor of one artifact, without its bytes.
|
|
3961
|
+
|
|
3962
|
+
`target` is a filename, an id or a descriptor. A filename means the latest
|
|
3963
|
+
version, unless you give `version`. A negative version counts back.
|
|
3964
|
+
"""
|
|
3965
|
+
payload = _host.get_attachment(*_attachment_target(target, version))
|
|
3966
|
+
return _attachment_row(json.loads(payload))
|
|
3967
|
+
|
|
3968
|
+
|
|
3969
|
+
def read_attachment(target, version=None):
|
|
3970
|
+
"""Return the bytes of one artifact. `target` works as in `get_attachment`."""
|
|
3971
|
+
encoded = _host.read_attachment(*_attachment_target(target, version))
|
|
3972
|
+
return base64.b64decode(encoded) if encoded else b""
|
|
3973
|
+
|
|
3974
|
+
|
|
3975
|
+
def show_attachment(target, version=None):
|
|
3976
|
+
"""Show a stored image to the model for the next request only.
|
|
3977
|
+
|
|
3978
|
+
`target` works as in `get_attachment`. Returns the id, filename, media_type
|
|
3979
|
+
and size of the queued image.
|
|
3980
|
+
"""
|
|
3981
|
+
shown = list(_host.show_attachment(*_attachment_target(target, version)))
|
|
3982
|
+
return dict(zip(("id", "filename", "media_type", "size"), shown, strict=True))
|
|
3983
|
+
|
|
3984
|
+
|
|
3985
|
+
def read_session(target=None):
|
|
3986
|
+
"""Read one whole session, exactly as the sandbox `read_session` does.
|
|
3987
|
+
|
|
3988
|
+
Without `target`, it reads the session that runs the extension. `target` takes a
|
|
3989
|
+
bare id, an unambiguous prefix or the copied `vis_session_id#<uuid>` marker. The
|
|
3990
|
+
`introspection` toggle does not apply, because an extension is trusted code.
|
|
3991
|
+
|
|
3992
|
+
Returns the same string-keyed dict: session, current_turn, failures, diagnosis,
|
|
3993
|
+
session_forks, turn_retries, usage and transcript.
|
|
3994
|
+
"""
|
|
3995
|
+
return json.loads(_host.read_session(None if target is None else str(target)))
|
|
3996
|
+
|
|
3997
|
+
|
|
3998
|
+
def list_sessions(search=None):
|
|
3999
|
+
"""List sessions newest first, exactly as the sandbox `list_sessions` does.
|
|
4000
|
+
|
|
4001
|
+
With `search`, it runs the session search of the TUI and the Companion app. The
|
|
4002
|
+
rows come in rank order: title, request, reply, then thinking, newest first in
|
|
4003
|
+
each rank. Keep that order. The `introspection` toggle does not apply.
|
|
4004
|
+
|
|
4005
|
+
Returns string-keyed rows: id, channel, title, goal, turn_count, created_at and
|
|
4006
|
+
modified_at. A match adds rank, the is_in_* flags and optional snippets.
|
|
4007
|
+
"""
|
|
4008
|
+
return json.loads(_host.list_sessions(None if search is None else str(search)))
|
|
4009
|
+
|
|
4010
|
+
|
|
3799
4011
|
# -- Live views ---------------------------------------------------------------
|
|
3800
4012
|
# A form PAUSES a run to collect values; a live view REPORTS on work already
|
|
3801
4013
|
# running. Nothing here blocks: a push crosses to the engine, every mounted
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""`vis.attach` and the attachment readers, against the outside host."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
import blockether.vis.extension as vis
|
|
7
|
+
import pytest
|
|
8
|
+
from blockether.vis import _outside
|
|
9
|
+
|
|
10
|
+
PNG = b"\x89PNG\r\n\x1a\n" + b"\0" * 16
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture(autouse=True)
|
|
14
|
+
def _empty_store(monkeypatch):
|
|
15
|
+
monkeypatch.setattr(_outside, "_ARTIFACTS", [])
|
|
16
|
+
monkeypatch.setattr(_outside, "_ARTIFACT_BYTES", {})
|
|
17
|
+
monkeypatch.setattr(vis, "_host", _outside.host)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_attach_stores_a_path_and_returns_its_descriptor(tmp_path):
|
|
21
|
+
source = tmp_path / "report.csv"
|
|
22
|
+
source.write_text("a,b\n1,2\n", encoding="utf-8")
|
|
23
|
+
|
|
24
|
+
row = vis.attach(source, label="Totals", commentable=True)
|
|
25
|
+
|
|
26
|
+
assert row["filename"] == "report.csv"
|
|
27
|
+
assert row["version"] == 1
|
|
28
|
+
assert row["media_type"] == "text/csv"
|
|
29
|
+
assert row["kind"] == "file"
|
|
30
|
+
assert row["size"] == 8
|
|
31
|
+
assert row["commentable"] is True
|
|
32
|
+
assert vis.read_attachment(row) == b"a,b\n1,2\n"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_same_filename_adds_a_version_and_targets_resolve_it():
|
|
36
|
+
first = vis.attach(PNG, "chart.png")
|
|
37
|
+
second = vis.attach(PNG + b"x", "chart.png", audience="user")
|
|
38
|
+
|
|
39
|
+
assert second["version"] == 2
|
|
40
|
+
assert second["kind"] == "image"
|
|
41
|
+
assert [r["version"] for r in vis.list_attachments("chart.png")] == [1, 2]
|
|
42
|
+
assert vis.get_attachment("chart.png")["id"] == second["id"]
|
|
43
|
+
assert vis.get_attachment("chart.png", version=-2)["id"] == first["id"]
|
|
44
|
+
assert vis.get_attachment(first["id"])["version"] == 1
|
|
45
|
+
assert vis.read_attachment("chart.png", 1) == PNG
|
|
46
|
+
assert vis.show_attachment(second) == {
|
|
47
|
+
"id": second["id"],
|
|
48
|
+
"filename": "chart.png",
|
|
49
|
+
"media_type": "image/png",
|
|
50
|
+
"size": len(PNG) + 1,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_attach_sends_one_options_map_to_the_host(monkeypatch):
|
|
55
|
+
sent = []
|
|
56
|
+
|
|
57
|
+
class Host:
|
|
58
|
+
def attach(self, options):
|
|
59
|
+
sent.append(options)
|
|
60
|
+
return json.dumps(
|
|
61
|
+
{"id": "a1", "filename": options["filename"], "media-type": "x/y"}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
monkeypatch.setattr(vis, "_host", Host())
|
|
65
|
+
|
|
66
|
+
# `.txt` maps to text/plain on every Python version; `.md` does not before 3.13.
|
|
67
|
+
assert vis.attach(b"data", "notes.txt", media_type="text/markdown") == {
|
|
68
|
+
"id": "a1",
|
|
69
|
+
"filename": "notes.txt",
|
|
70
|
+
"media_type": "x/y",
|
|
71
|
+
}
|
|
72
|
+
assert sent == [
|
|
73
|
+
{
|
|
74
|
+
"base64": base64.b64encode(b"data").decode("ascii"),
|
|
75
|
+
"filename": "notes.txt",
|
|
76
|
+
"media_type": "text/markdown",
|
|
77
|
+
"media_type_hint": "text/plain",
|
|
78
|
+
"audience": "both",
|
|
79
|
+
"commentable": False,
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_attach_refuses_wrong_sources_and_options(tmp_path):
|
|
85
|
+
with pytest.raises(TypeError, match="source must be a path"):
|
|
86
|
+
vis.attach(object())
|
|
87
|
+
with pytest.raises(IsADirectoryError):
|
|
88
|
+
vis.attach(tmp_path)
|
|
89
|
+
with pytest.raises(FileNotFoundError, match="no such file"):
|
|
90
|
+
vis.attach(tmp_path / "missing.png")
|
|
91
|
+
with pytest.raises(ValueError, match="audience"):
|
|
92
|
+
vis.attach(PNG, "a.png", audience="everyone")
|
|
93
|
+
with pytest.raises(TypeError, match="commentable"):
|
|
94
|
+
vis.attach(PNG, "a.png", commentable="yes")
|
|
95
|
+
with pytest.raises(ValueError, match="empty payload"):
|
|
96
|
+
vis.attach(b"", "a.png")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_readers_name_an_unknown_target_and_a_non_image():
|
|
100
|
+
vis.attach(b"text", "notes.txt")
|
|
101
|
+
|
|
102
|
+
with pytest.raises(LookupError, match="unknown id or filename"):
|
|
103
|
+
vis.get_attachment("other.txt")
|
|
104
|
+
with pytest.raises(LookupError, match="has no version 3"):
|
|
105
|
+
vis.read_attachment("notes.txt", 3)
|
|
106
|
+
with pytest.raises(ValueError, match="not an image"):
|
|
107
|
+
vis.show_attachment("notes.txt")
|
|
108
|
+
with pytest.raises(TypeError, match="version must be an integer"):
|
|
109
|
+
vis.get_attachment("notes.txt", "1")
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""`vis.read_session` and `vis.list_sessions` use the canonical host session reads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import blockether.vis.extension as vis
|
|
8
|
+
import pytest
|
|
9
|
+
from blockether.vis import _outside
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_answers_the_host_read_as_one_string_keyed_dict(monkeypatch):
|
|
13
|
+
targets = []
|
|
14
|
+
data = {
|
|
15
|
+
"session": {"id": "6fccb4a2-bf20-448c-ac4f-9d170983c0f3"},
|
|
16
|
+
"transcript": {"turns": [{"position": 1, "user_request": "Fix the typo."}]},
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def read_session(target):
|
|
20
|
+
targets.append(target)
|
|
21
|
+
return json.dumps(data)
|
|
22
|
+
|
|
23
|
+
monkeypatch.setattr(vis._host, "read_session", read_session)
|
|
24
|
+
assert vis.read_session() == data
|
|
25
|
+
assert vis.read_session("vis_session_id#6fccb4a2") == data
|
|
26
|
+
assert targets == [None, "vis_session_id#6fccb4a2"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_answers_the_host_search_rows_in_their_rank_order(monkeypatch):
|
|
30
|
+
searches = []
|
|
31
|
+
rows = [
|
|
32
|
+
{"id": "b", "title": "Fix the typo", "rank": 0, "is_in_title": True},
|
|
33
|
+
{"id": "a", "title": "Release", "rank": 1, "is_in_request": True},
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
def list_sessions(search):
|
|
37
|
+
searches.append(search)
|
|
38
|
+
return json.dumps(rows)
|
|
39
|
+
|
|
40
|
+
monkeypatch.setattr(vis._host, "list_sessions", list_sessions)
|
|
41
|
+
assert vis.list_sessions() == rows
|
|
42
|
+
assert vis.list_sessions(search="typo") == rows
|
|
43
|
+
assert searches == [None, "typo"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.parametrize(
|
|
47
|
+
"call, message",
|
|
48
|
+
[
|
|
49
|
+
(vis.read_session, "vis.read_session needs a bound Vis session"),
|
|
50
|
+
(vis.list_sessions, "vis.list_sessions needs a bound Vis session"),
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
def test_refuses_outside_a_vis_session(call, message):
|
|
54
|
+
with pytest.raises(_outside.Refused, match=message):
|
|
55
|
+
call()
|
|
@@ -68,6 +68,23 @@ def test_parent_nests_a_setting():
|
|
|
68
68
|
vis.Setting(id="test_detail", label="Test detail", default=False, parent=" ")
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
# Regression for #369: a setting declares a change hook that the host calls.
|
|
72
|
+
def test_on_change_declares_a_callable_hook():
|
|
73
|
+
def record(old, new, scope):
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
plain = vis.Setting(id="test_feature", label="Test feature", default=True)
|
|
77
|
+
hooked = vis.Setting(
|
|
78
|
+
id="test_feature", label="Test feature", default=True, on_change=record
|
|
79
|
+
)
|
|
80
|
+
assert "on_change" not in plain._spec()
|
|
81
|
+
assert hooked._spec()["on_change"] is record
|
|
82
|
+
with pytest.raises(TypeError):
|
|
83
|
+
vis.Setting(
|
|
84
|
+
id="test_feature", label="Test feature", default=True, on_change="record"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
71
88
|
# Regression for #322 and #323: a 300-character description is valid, and a
|
|
72
89
|
# longer one names the setting, the field, the rule and the limit.
|
|
73
90
|
def test_description_limit_and_actionable_error():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-arguments.json
RENAMED
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-cases.json
RENAMED
|
File without changes
|
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-execution.json
RENAMED
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-groups.json
RENAMED
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-read-session.json
RENAMED
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-reads.json
RENAMED
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-results.json
RENAMED
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/activity-tables.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/live-primitives.json
RENAMED
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/contracts/fixtures/session-health.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/examples/greeter/tests/test_greeter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/engine/_extensions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/evals/trajectory.py
RENAMED
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/src/blockether/vis/extension_package.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_agent_extension_lifecycle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vis_agent-0.2.38.dev7570 → vis_agent-0.2.39.dev7588}/tests/test_client_extensions_engine.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|