vis-agent 0.2.32.dev7313__tar.gz → 0.2.32.dev7344__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.32.dev7313 → vis_agent-0.2.32.dev7344}/PKG-INFO +1 -1
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/pyproject.toml +1 -1
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/_outside.py +8 -2
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/engine/_agent.py +8 -8
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/engine/_client.py +26 -7
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/engine/_local.py +10 -6
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/extension.py +93 -54
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/extension_package.py +9 -4
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/rooms.py +1 -1
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/views.py +1 -0
- vis_agent-0.2.32.dev7344/tests/test_sdk_typing.py +108 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/.gitignore +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/LICENSE +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/README.md +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-arguments.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-cases.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-copy.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-execution.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-groups.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-read-session.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-reads.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-results.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-tables.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/diff.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/live-primitives.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/session-health.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/view.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/activity.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/agents.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/automations.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/common.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/config.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/content.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/council.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/diff.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/gateway.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/improve.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/provider.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/rooms.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/symbol.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/toggle.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/schema/view.json +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/examples/greeter/tests/test_greeter.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/hatch_build.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/__init__.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/_contracts.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/_wire.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/activity.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/engine/__init__.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/engine/_agents.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/engine/_council.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/engine/_extensions.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/py.typed +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/conftest.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_activity.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_agent.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_agent_extension_lifecycle.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_agent_extensions.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_agents.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_api_guides.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_authoring.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_catalog.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_catalog_recipe.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_ci_report_recipe.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_client.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_client_extensions_engine.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_code_quality_recipe.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_contracts.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_council.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_declarations.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_engine.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_engine_docstrings.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_extension_package.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_extension_reference.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_gateway_performance.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_goals.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_live_primitives.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_live_wait.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_local.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_method_typing.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_monitor_recipe.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_namespace.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_outside.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_providers.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_record_arguments.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_registration.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_repair_hooks.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_result_sequence.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_rooms.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_scoped_settings.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_sdk_bootstrap.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_sdk_guide.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_structured_live.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_symbol_contract.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_testing.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/tests/test_tools.py +0 -0
- {vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/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.32.
|
|
3
|
+
Version: 0.2.32.dev7344
|
|
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
|
|
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "vis-agent"
|
|
11
|
-
version = "0.2.32.
|
|
11
|
+
version = "0.2.32.dev7344"
|
|
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"
|
|
@@ -21,11 +21,15 @@ import uuid
|
|
|
21
21
|
from copy import deepcopy
|
|
22
22
|
from datetime import date
|
|
23
23
|
from pathlib import Path
|
|
24
|
+
from typing import TYPE_CHECKING, Any
|
|
24
25
|
|
|
25
26
|
from blockether.vis import _contracts
|
|
26
27
|
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from blockether.vis.extension import Host
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
|
|
32
|
+
def check_host(host) -> Host:
|
|
29
33
|
"""Refuse a host that does not answer every implemented host operation.
|
|
30
34
|
|
|
31
35
|
Returns the host, so a constructor can `return check_host(built)`. An incomplete
|
|
@@ -412,7 +416,7 @@ class _Run:
|
|
|
412
416
|
def _result(run, stage, **extra):
|
|
413
417
|
code = run.poll()
|
|
414
418
|
now = time.time()
|
|
415
|
-
result = dict.fromkeys(_SHELL_RESULT_KEYS)
|
|
419
|
+
result: dict[str, Any] = dict.fromkeys(_SHELL_RESULT_KEYS)
|
|
416
420
|
out, next_offset, omitted = run.read(
|
|
417
421
|
extra.pop("_offset", None), extra.pop("_limit", None)
|
|
418
422
|
)
|
|
@@ -1083,6 +1087,7 @@ def _live_apply(view, op):
|
|
|
1083
1087
|
complaint = _live_check_node(spec, _live_ids(view["nodes"]))
|
|
1084
1088
|
if complaint:
|
|
1085
1089
|
raise Refused(complaint)
|
|
1090
|
+
assert isinstance(spec, dict) # _live_check_node refuses any other spec.
|
|
1086
1091
|
found = _live_find(view["nodes"], op.get("after"))
|
|
1087
1092
|
# A new node joins the row that holds the node it named, not the top of
|
|
1088
1093
|
# the view: `after` is a sibling, so the arrangement stays the one declared.
|
|
@@ -1234,6 +1239,7 @@ def live(envelope_json):
|
|
|
1234
1239
|
complaint = _check_view(view)
|
|
1235
1240
|
if complaint:
|
|
1236
1241
|
raise Refused(complaint)
|
|
1242
|
+
assert isinstance(view, dict) # _check_view refuses any other view.
|
|
1237
1243
|
view_id = str(uuid.uuid4())
|
|
1238
1244
|
held = {"view_id": view_id, "view": json.loads(json.dumps(view)), "seq": 0}
|
|
1239
1245
|
held["condition"] = threading.Condition()
|
|
@@ -11,7 +11,7 @@ from typing import Any, TypeVar, overload
|
|
|
11
11
|
from jsonschema import Draft202012Validator, validators
|
|
12
12
|
from jsonschema.exceptions import ValidationError, best_match
|
|
13
13
|
from pydantic import BaseModel
|
|
14
|
-
from pydantic_core import SchemaError, SchemaValidator, core_schema
|
|
14
|
+
from pydantic_core import ErrorDetails, SchemaError, SchemaValidator, core_schema
|
|
15
15
|
from pydantic_core import ValidationError as PydanticValidationError
|
|
16
16
|
from referencing import Registry
|
|
17
17
|
from referencing.exceptions import Unresolvable
|
|
@@ -173,7 +173,7 @@ def _schema_problems(error: ValidationError, show_input: bool):
|
|
|
173
173
|
"""Describe a JSON Schema error without printing whole objects or arrays."""
|
|
174
174
|
path = tuple(error.absolute_path)
|
|
175
175
|
keyword, expected, value = error.validator, error.validator_value, error.instance
|
|
176
|
-
if keyword == "required" and isinstance(value, dict):
|
|
176
|
+
if keyword == "required" and isinstance(value, dict) and isinstance(expected, list):
|
|
177
177
|
for name in expected:
|
|
178
178
|
if name not in value:
|
|
179
179
|
yield _problem("schema", (*path, name), "required property is missing")
|
|
@@ -352,7 +352,7 @@ class _ResponseContract:
|
|
|
352
352
|
for item in details
|
|
353
353
|
)
|
|
354
354
|
|
|
355
|
-
def _pydantic_message(self, item:
|
|
355
|
+
def _pydantic_message(self, item: ErrorDetails) -> str:
|
|
356
356
|
value = item.get("input")
|
|
357
357
|
if self.show_input and item["type"] != "missing" and _is_scalar(value):
|
|
358
358
|
return f"{item['msg']} (input: {_brief(value)})"
|
|
@@ -426,7 +426,7 @@ class Agent:
|
|
|
426
426
|
self._session = self.execution_layer.create_session(
|
|
427
427
|
**self._session_options
|
|
428
428
|
)
|
|
429
|
-
self._mount_extensions(self._extensions)
|
|
429
|
+
self._mount_extensions(self._session, self._extensions)
|
|
430
430
|
except BaseException:
|
|
431
431
|
try:
|
|
432
432
|
self.close()
|
|
@@ -435,13 +435,13 @@ class Agent:
|
|
|
435
435
|
raise
|
|
436
436
|
return self._session
|
|
437
437
|
|
|
438
|
-
def _mount_extensions(self, extensions):
|
|
438
|
+
def _mount_extensions(self, session: Session, extensions):
|
|
439
439
|
if extensions.manifest:
|
|
440
440
|
self.execution_layer._ensure_client_lease()
|
|
441
441
|
self.execution_layer.put_session_client_extensions(
|
|
442
|
-
|
|
442
|
+
session.id, body={"extensions": extensions.manifest}
|
|
443
443
|
)
|
|
444
|
-
self.execution_layer._client_extensions[
|
|
444
|
+
self.execution_layer._client_extensions[session.id] = extensions
|
|
445
445
|
|
|
446
446
|
def register_extension(self, extension):
|
|
447
447
|
"""Add an application-owned Extension before this Agent's first request.
|
|
@@ -458,7 +458,7 @@ class Agent:
|
|
|
458
458
|
raise RuntimeError("register extensions before the Agent's first request")
|
|
459
459
|
candidate = ClientExtensions((*self._extensions.declarations, extension))
|
|
460
460
|
if self._session is not None:
|
|
461
|
-
self._mount_extensions(candidate)
|
|
461
|
+
self._mount_extensions(self._session, candidate)
|
|
462
462
|
self._extensions = candidate
|
|
463
463
|
|
|
464
464
|
def send(self, request: str, **options) -> Turn:
|
|
@@ -7,6 +7,7 @@ means that a submitted mutation was rolled back.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
import inspect
|
|
10
11
|
import json
|
|
11
12
|
import math
|
|
12
13
|
import threading
|
|
@@ -135,7 +136,7 @@ def _field(data: Any, name: str, kind: type) -> Any:
|
|
|
135
136
|
value = data.get(name) if isinstance(data, dict) else None
|
|
136
137
|
if type(value) is not kind or (kind is str and not value):
|
|
137
138
|
raise ProtocolError(f"invalid response field: {name}")
|
|
138
|
-
if
|
|
139
|
+
if type(value) is int and value < 0:
|
|
139
140
|
raise ProtocolError(f"invalid response field: {name}")
|
|
140
141
|
return value
|
|
141
142
|
|
|
@@ -286,7 +287,7 @@ class ExecutionLayer(ABC):
|
|
|
286
287
|
@abstractmethod
|
|
287
288
|
def _open(
|
|
288
289
|
self, method, route, *, query=None, body=None, content=None, timeout=None
|
|
289
|
-
):
|
|
290
|
+
) -> Any:
|
|
290
291
|
"""Run one transport exchange. Do not retry mutations."""
|
|
291
292
|
|
|
292
293
|
def _ensure_client_lease(self, *, pid: int | None = None) -> str:
|
|
@@ -297,7 +298,8 @@ class ExecutionLayer(ABC):
|
|
|
297
298
|
if pid is not None:
|
|
298
299
|
body["pid"] = pid
|
|
299
300
|
lease = self._request("POST", "/v1/clients", body=body).json()
|
|
300
|
-
|
|
301
|
+
client_id: str = _field(lease, "client_id", str)
|
|
302
|
+
self._lease = client_id
|
|
301
303
|
return self._lease
|
|
302
304
|
|
|
303
305
|
def __enter__(self):
|
|
@@ -364,7 +366,8 @@ class ExecutionLayer(ABC):
|
|
|
364
366
|
f"maximum is {_MAX_DECISION_ARCHIVE} bytes"
|
|
365
367
|
)
|
|
366
368
|
names = {s[1:] for s in route.split("/") if s.startswith(":")}
|
|
367
|
-
|
|
369
|
+
path = path or {}
|
|
370
|
+
if set(path) != names:
|
|
368
371
|
raise ValueError("path parameters do not match route")
|
|
369
372
|
resolved = "/".join(
|
|
370
373
|
_segment(path[s[1:]]) if s.startswith(":") else s for s in route.split("/")
|
|
@@ -2995,6 +2998,9 @@ class GatewayClient(ExecutionLayer):
|
|
|
2995
2998
|
if upload_sha256 is not None:
|
|
2996
2999
|
headers["Content-Type"] = "application/zip"
|
|
2997
3000
|
headers["Transfer-Encoding"] = "chunked"
|
|
3001
|
+
# _request sends a hash only with a bounded stream and its length.
|
|
3002
|
+
assert content is not None and not isinstance(content, bytes)
|
|
3003
|
+
assert upload_length is not None
|
|
2998
3004
|
content = _UploadStream(content, upload_length)
|
|
2999
3005
|
headers["X-Content-SHA256"] = upload_sha256
|
|
3000
3006
|
url = self._url + route + ("?" + urlencode(query) if query else "")
|
|
@@ -3136,7 +3142,7 @@ class Session:
|
|
|
3136
3142
|
client: ExecutionLayer
|
|
3137
3143
|
id: str
|
|
3138
3144
|
|
|
3139
|
-
def _call(self, method, suffix="", **kwargs):
|
|
3145
|
+
def _call(self, method, suffix="", **kwargs) -> Any:
|
|
3140
3146
|
path = {"sid": self.id, **kwargs.pop("path", {})}
|
|
3141
3147
|
response = self.client._request(
|
|
3142
3148
|
method, "/v1/sessions/:sid" + suffix, path=path, **kwargs
|
|
@@ -3476,7 +3482,7 @@ class _EventStream:
|
|
|
3476
3482
|
if self._raw is not None:
|
|
3477
3483
|
self._raw.close()
|
|
3478
3484
|
# A pipe failure can close its owning client from inside this generator.
|
|
3479
|
-
if
|
|
3485
|
+
if inspect.getgeneratorstate(self._iterator) != inspect.GEN_RUNNING:
|
|
3480
3486
|
self._iterator.close()
|
|
3481
3487
|
self.client._streams.discard(self)
|
|
3482
3488
|
|
|
@@ -3533,8 +3539,16 @@ class _EventStream:
|
|
|
3533
3539
|
def _terminal(self, event):
|
|
3534
3540
|
return False
|
|
3535
3541
|
|
|
3542
|
+
def _endpoint(self):
|
|
3543
|
+
"""Return the route and query of this subscription."""
|
|
3544
|
+
raise NotImplementedError
|
|
3545
|
+
|
|
3546
|
+
def _accept(self, name, value):
|
|
3547
|
+
"""Return the event of one frame, or None to skip the frame."""
|
|
3548
|
+
raise NotImplementedError
|
|
3536
3549
|
|
|
3537
|
-
|
|
3550
|
+
|
|
3551
|
+
class _PollingEvents(_EventStream):
|
|
3538
3552
|
"""Poll finite event pages so callback code runs on the SDK calling thread."""
|
|
3539
3553
|
|
|
3540
3554
|
def _iterate(self):
|
|
@@ -3558,6 +3572,10 @@ class _PollingEvents:
|
|
|
3558
3572
|
def _pump(self):
|
|
3559
3573
|
pass
|
|
3560
3574
|
|
|
3575
|
+
def _page(self):
|
|
3576
|
+
"""Return the next finite page of `(event name, value)` pairs."""
|
|
3577
|
+
raise NotImplementedError
|
|
3578
|
+
|
|
3561
3579
|
|
|
3562
3580
|
class Events(_EventStream):
|
|
3563
3581
|
"""Iterate typed session events, with replay and bounded reconnection.
|
|
@@ -3604,6 +3622,7 @@ class Events(_EventStream):
|
|
|
3604
3622
|
if name is not None and name != event.type:
|
|
3605
3623
|
raise ProtocolError("event name disagrees with payload")
|
|
3606
3624
|
seq = event.cursor if event.type == "subscription.ready" else event.seq
|
|
3625
|
+
assert seq is not None # Event.from_wire requires the field of each type.
|
|
3607
3626
|
if event.type == "subscription.ready" or seq > self.cursor:
|
|
3608
3627
|
self.cursor = seq
|
|
3609
3628
|
return event
|
|
@@ -117,11 +117,13 @@ class LocalEngine(ExecutionLayer):
|
|
|
117
117
|
raise NotADirectoryError(str(path))
|
|
118
118
|
return {"root": str(path)}
|
|
119
119
|
|
|
120
|
-
def _ensure_client_lease(self) -> str:
|
|
120
|
+
def _ensure_client_lease(self, *, pid: int | None = None) -> str:
|
|
121
121
|
# Stdio shares the application host; its PID keeps idle callbacks alive.
|
|
122
|
-
return super()._ensure_client_lease(pid=os.getpid())
|
|
122
|
+
return super()._ensure_client_lease(pid=os.getpid() if pid is None else pid)
|
|
123
123
|
|
|
124
124
|
def _read_line(self, deadline):
|
|
125
|
+
# connect() starts the engine with piped stdio before any read.
|
|
126
|
+
assert self._process is not None and self._process.stdout is not None
|
|
125
127
|
while b"\n" not in self._buffer:
|
|
126
128
|
remaining = deadline - time.monotonic()
|
|
127
129
|
if (
|
|
@@ -176,6 +178,7 @@ class LocalEngine(ExecutionLayer):
|
|
|
176
178
|
stderr=stderr,
|
|
177
179
|
start_new_session=True,
|
|
178
180
|
)
|
|
181
|
+
assert self._process.stdin is not None # Popen got stdin=PIPE.
|
|
179
182
|
os.set_blocking(self._process.stdin.fileno(), False)
|
|
180
183
|
hello = self._read_line(time.monotonic() + self._startup_timeout)
|
|
181
184
|
if (
|
|
@@ -197,6 +200,7 @@ class LocalEngine(ExecutionLayer):
|
|
|
197
200
|
self, method, route, *, query=None, body=None, content=None, timeout=None
|
|
198
201
|
):
|
|
199
202
|
self.connect()
|
|
203
|
+
assert self._process is not None and self._process.stdin is not None
|
|
200
204
|
deadline = time.monotonic() + _duration(
|
|
201
205
|
self.timeout if timeout is None else timeout
|
|
202
206
|
)
|
|
@@ -296,14 +300,14 @@ class LocalEngine(ExecutionLayer):
|
|
|
296
300
|
if process.stdout:
|
|
297
301
|
process.stdout.close()
|
|
298
302
|
if self._home is not None:
|
|
299
|
-
self._stderr_tail = self._stderr_end()
|
|
303
|
+
self._stderr_tail = self._stderr_end(self._home.name)
|
|
300
304
|
self._home.cleanup()
|
|
301
305
|
self._cleanup_complete = True
|
|
302
306
|
|
|
303
|
-
def _stderr_end(self):
|
|
304
|
-
"""Return the end of the engine's stderr log
|
|
307
|
+
def _stderr_end(self, home):
|
|
308
|
+
"""Return the end of the engine's stderr log in `home`, from a line start."""
|
|
305
309
|
try:
|
|
306
|
-
with open(Path(
|
|
310
|
+
with open(Path(home) / "stderr.log", "rb") as stream:
|
|
307
311
|
size = stream.seek(0, os.SEEK_END)
|
|
308
312
|
stream.seek(max(0, size - _STDERR_TAIL))
|
|
309
313
|
end = stream.read()
|
|
@@ -27,6 +27,7 @@ from typing import (
|
|
|
27
27
|
Annotated,
|
|
28
28
|
Any,
|
|
29
29
|
ClassVar,
|
|
30
|
+
Generic,
|
|
30
31
|
Literal,
|
|
31
32
|
Optional,
|
|
32
33
|
Protocol,
|
|
@@ -39,6 +40,9 @@ from typing import (
|
|
|
39
40
|
runtime_checkable,
|
|
40
41
|
)
|
|
41
42
|
|
|
43
|
+
# A setting value: `bool` for a boolean setting, `str` for a choice setting.
|
|
44
|
+
_SettingValue = TypeVar("_SettingValue", bool, str)
|
|
45
|
+
|
|
42
46
|
|
|
43
47
|
@runtime_checkable
|
|
44
48
|
class Host(Protocol):
|
|
@@ -46,47 +50,60 @@ class Host(Protocol):
|
|
|
46
50
|
|
|
47
51
|
def workspace_root(self) -> str:
|
|
48
52
|
"""Read the active session working copy, or the outside process directory."""
|
|
53
|
+
...
|
|
49
54
|
|
|
50
55
|
def setting_declaration(self, spec: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
51
56
|
"""Validate a declaration and fill defaults from the contract schema."""
|
|
57
|
+
...
|
|
52
58
|
|
|
53
|
-
def setting(self, id: str, default:
|
|
54
|
-
"""Read one setting from the current callback snapshot
|
|
59
|
+
def setting(self, id: str, default: _SettingValue) -> _SettingValue:
|
|
60
|
+
"""Read one setting from the current callback snapshot, as the type of `default`."""
|
|
61
|
+
...
|
|
55
62
|
|
|
56
63
|
def state_get(self, key: str) -> Any:
|
|
57
64
|
"""Read one value out of the extension's durable state."""
|
|
65
|
+
...
|
|
58
66
|
|
|
59
67
|
def state_put(self, key: str, value: Any) -> Any:
|
|
60
68
|
"""Write one JSON value into the extension's durable state."""
|
|
69
|
+
...
|
|
61
70
|
|
|
62
71
|
def state_del(self, key: str) -> Any:
|
|
63
72
|
"""Drop one key from the extension's durable state."""
|
|
73
|
+
...
|
|
64
74
|
|
|
65
75
|
def state_keys(self) -> Any:
|
|
66
76
|
"""List every key the extension's durable state holds."""
|
|
77
|
+
...
|
|
67
78
|
|
|
68
79
|
def log(self, level: str, message: str) -> Any:
|
|
69
80
|
"""Emit one engine log line at a level."""
|
|
81
|
+
...
|
|
70
82
|
|
|
71
83
|
def notify(self, text: str, level: str) -> Any:
|
|
72
84
|
"""Show one notification on the user's channel."""
|
|
85
|
+
...
|
|
73
86
|
|
|
74
87
|
def council_wake(self, options: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
75
88
|
"""Publish to the bound session. Only an eligible managed subagent wakes itself."""
|
|
89
|
+
...
|
|
76
90
|
|
|
77
91
|
def shell(self, options: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
78
92
|
"""Run one shell operation. Return the contract's result shape."""
|
|
93
|
+
...
|
|
79
94
|
|
|
80
95
|
def jailed_shell(self, options: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
81
96
|
"""Run one shell op inside the workspace jail."""
|
|
97
|
+
...
|
|
82
98
|
|
|
83
99
|
def request_input(
|
|
84
100
|
self,
|
|
85
101
|
request_json: str,
|
|
86
102
|
validator_arities_json: str,
|
|
87
|
-
run_validator: Callable[[str, str], str],
|
|
103
|
+
run_validator: Callable[[str, int, str, str], str],
|
|
88
104
|
) -> str:
|
|
89
105
|
"""Ask the human, and block until the answer settles or is cancelled."""
|
|
106
|
+
...
|
|
90
107
|
|
|
91
108
|
def live(self, envelope_json: str) -> str:
|
|
92
109
|
"""Open, patch, read or close a live view.
|
|
@@ -96,28 +113,34 @@ class Host(Protocol):
|
|
|
96
113
|
An unchanged timeout returns is_open=True, timed_out=True, without view.
|
|
97
114
|
Ordinary state reads and changed waits return the current view with seq.
|
|
98
115
|
"""
|
|
116
|
+
...
|
|
99
117
|
|
|
100
118
|
def activity(self, presentation: dict[str, Any]) -> bool:
|
|
101
119
|
"""Replace the running symbol's headline, summary, content and sections."""
|
|
120
|
+
...
|
|
102
121
|
|
|
103
122
|
def reveal_secret(self, handle: str) -> Any:
|
|
104
123
|
"""Resolve a `vis-secret:` handle to its plaintext."""
|
|
124
|
+
...
|
|
105
125
|
|
|
106
126
|
def forget_secret(self, handle: str) -> Any:
|
|
107
127
|
"""Drop the plaintext a secret handle stands for."""
|
|
128
|
+
...
|
|
108
129
|
|
|
109
130
|
def declare_env(self, declarations_json: str) -> str:
|
|
110
131
|
"""Resolve the environment variables the extension declared."""
|
|
132
|
+
...
|
|
111
133
|
|
|
112
134
|
def call_tool(
|
|
113
135
|
self, tool: str, args: Sequence[Any], kwargs: Mapping[str, Any]
|
|
114
136
|
) -> Any:
|
|
115
137
|
"""Run one active session tool by the name `python_execution` calls."""
|
|
138
|
+
...
|
|
116
139
|
|
|
117
140
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
# The engine seeds `_host` into the module dict before exec.
|
|
142
|
+
_host: Host
|
|
143
|
+
if "_host" not in globals(): # Installed from PyPI: no host in the room, so bring one.
|
|
121
144
|
from blockether.vis import _outside as outside
|
|
122
145
|
|
|
123
146
|
_host = outside.host
|
|
@@ -253,7 +276,7 @@ def _tool_value(value):
|
|
|
253
276
|
return value
|
|
254
277
|
|
|
255
278
|
|
|
256
|
-
def _tool_data(value):
|
|
279
|
+
def _tool_data(value) -> Any:
|
|
257
280
|
"""One argument as JSON data. A dataclass instance travels as its public fields."""
|
|
258
281
|
if is_dataclass(value) and not isinstance(value, type):
|
|
259
282
|
return {
|
|
@@ -333,7 +356,7 @@ def _bounded_text(value, limit, name):
|
|
|
333
356
|
raise ValueError(f"{name} must be text of at most {limit} characters")
|
|
334
357
|
|
|
335
358
|
|
|
336
|
-
def _wire_value(value):
|
|
359
|
+
def _wire_value(value) -> Any:
|
|
337
360
|
if isinstance(value, Mapping):
|
|
338
361
|
return {key: _wire_value(item) for key, item in value.items()}
|
|
339
362
|
if isinstance(value, (tuple, list)):
|
|
@@ -341,7 +364,7 @@ def _wire_value(value):
|
|
|
341
364
|
return value
|
|
342
365
|
|
|
343
366
|
|
|
344
|
-
def _freeze_config(value):
|
|
367
|
+
def _freeze_config(value) -> Any:
|
|
345
368
|
if isinstance(value, Mapping):
|
|
346
369
|
if not all(isinstance(key, str) for key in value):
|
|
347
370
|
raise TypeError("provider JSON keys must be strings")
|
|
@@ -359,6 +382,7 @@ def _freeze_config(value):
|
|
|
359
382
|
|
|
360
383
|
class _ActivityBlock:
|
|
361
384
|
__slots__ = ()
|
|
385
|
+
__dataclass_fields__: ClassVar[dict[str, Any]] # Each subclass is a dataclass.
|
|
362
386
|
type: ClassVar[str]
|
|
363
387
|
|
|
364
388
|
def to_wire(self) -> dict[str, Any]:
|
|
@@ -510,7 +534,9 @@ class ActivityProgress(_ActivityBlock):
|
|
|
510
534
|
if self.value is None and self.total is None:
|
|
511
535
|
return
|
|
512
536
|
if (
|
|
513
|
-
|
|
537
|
+
self.value is None
|
|
538
|
+
or self.total is None
|
|
539
|
+
or type(self.value) not in (int, float)
|
|
514
540
|
or type(self.total) not in (int, float)
|
|
515
541
|
or not math.isfinite(self.value)
|
|
516
542
|
or not math.isfinite(self.total)
|
|
@@ -539,8 +565,9 @@ ActivityBlock: TypeAlias = (
|
|
|
539
565
|
|
|
540
566
|
_ActivitySummaryFormat: TypeAlias = Literal["inline", "markdown"]
|
|
541
567
|
_ActivityVerdict: TypeAlias = Literal["passed", "failed"]
|
|
542
|
-
|
|
543
|
-
|
|
568
|
+
# The kind of work of one tool call. Annotate a tag in a variable with this type.
|
|
569
|
+
SymbolTag: TypeAlias = Literal["observation", "mutation", "verification", "external"]
|
|
570
|
+
_SYMBOL_TAGS = get_args(SymbolTag)
|
|
544
571
|
|
|
545
572
|
|
|
546
573
|
@dataclass(frozen=True, slots=True)
|
|
@@ -762,9 +789,10 @@ def _activity_call(fn, activity):
|
|
|
762
789
|
except Exception:
|
|
763
790
|
pass # Presentation must not alter the operation's result or error.
|
|
764
791
|
|
|
792
|
+
# Each branch declares `invoke`; only one of them runs.
|
|
765
793
|
if inspect.iscoroutinefunction(fn):
|
|
766
794
|
|
|
767
|
-
async def invoke(*args, **kwargs):
|
|
795
|
+
async def invoke(*args, **kwargs): # pyright: ignore[reportRedeclaration]
|
|
768
796
|
if activity.show_start:
|
|
769
797
|
render("start", args, kwargs)
|
|
770
798
|
try:
|
|
@@ -792,23 +820,24 @@ def _activity_call(fn, activity):
|
|
|
792
820
|
return wraps(fn)(invoke)
|
|
793
821
|
|
|
794
822
|
|
|
795
|
-
_registration = {"spec": None}
|
|
823
|
+
_registration: dict[str, Any] = {"spec": None}
|
|
796
824
|
|
|
797
825
|
|
|
798
826
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
799
|
-
class Setting:
|
|
827
|
+
class Setting(Generic[_SettingValue]):
|
|
800
828
|
"""Declare a boolean or choice setting shared by the app and TUI.
|
|
801
829
|
|
|
802
830
|
`scopes` allows any non-empty combination of global, project, group and session.
|
|
803
831
|
Omitting it uses the contract's global-only default. A project extension exists
|
|
804
832
|
only in its project, so there `global` means the whole project. The setting shows
|
|
805
833
|
in its extension's own settings section. `value()` reads the current callback's
|
|
806
|
-
response snapshot.
|
|
834
|
+
response snapshot. Its type follows `default`: `bool` for a boolean setting, `str`
|
|
835
|
+
for a choice setting. Outside Vis, it returns the declared default.
|
|
807
836
|
"""
|
|
808
837
|
|
|
809
838
|
id: str
|
|
810
839
|
label: str
|
|
811
|
-
default:
|
|
840
|
+
default: _SettingValue
|
|
812
841
|
type: str = "boolean"
|
|
813
842
|
choices: Sequence[str] = ()
|
|
814
843
|
scopes: Sequence[str] | None = None
|
|
@@ -850,7 +879,7 @@ class Setting:
|
|
|
850
879
|
spec["description"] = self.description
|
|
851
880
|
return spec
|
|
852
881
|
|
|
853
|
-
def value(self) ->
|
|
882
|
+
def value(self) -> _SettingValue:
|
|
854
883
|
"""Read this setting in the current callback without changing any scope."""
|
|
855
884
|
return _host.setting(self.id, self.default)
|
|
856
885
|
|
|
@@ -1863,8 +1892,9 @@ def sequence(*, field: str) -> Callable[[_SequenceClass], _SequenceClass]:
|
|
|
1863
1892
|
)
|
|
1864
1893
|
if field not in {item.name for item in fields(cls)}:
|
|
1865
1894
|
raise ValueError(f"sequence field {field!r} is not a dataclass field")
|
|
1866
|
-
cls
|
|
1867
|
-
|
|
1895
|
+
# Pyright narrows `cls` to a plain dataclass type and cannot see the marker.
|
|
1896
|
+
cls.__vis_sequence_field__ = field # pyright: ignore[reportAttributeAccessIssue]
|
|
1897
|
+
return cls # pyright: ignore[reportReturnType]
|
|
1868
1898
|
|
|
1869
1899
|
return decorate
|
|
1870
1900
|
|
|
@@ -1879,11 +1909,16 @@ class _MethodDecorator(Protocol):
|
|
|
1879
1909
|
def __call__(self, actual: _Method) -> _Method: ...
|
|
1880
1910
|
|
|
1881
1911
|
|
|
1912
|
+
def _method_function(value: Any) -> Any:
|
|
1913
|
+
"""Return the function under a `staticmethod` or `classmethod`, else `value`."""
|
|
1914
|
+
return value.__func__ if isinstance(value, (staticmethod, classmethod)) else value
|
|
1915
|
+
|
|
1916
|
+
|
|
1882
1917
|
@overload
|
|
1883
1918
|
def method(
|
|
1884
1919
|
fn: _Method,
|
|
1885
1920
|
*,
|
|
1886
|
-
tag:
|
|
1921
|
+
tag: SymbolTag = "observation",
|
|
1887
1922
|
is_hidden: bool = False,
|
|
1888
1923
|
activity: Activity | None = None,
|
|
1889
1924
|
) -> _Method: ...
|
|
@@ -1893,19 +1928,19 @@ def method(
|
|
|
1893
1928
|
def method(
|
|
1894
1929
|
fn: None = None,
|
|
1895
1930
|
*,
|
|
1896
|
-
tag:
|
|
1931
|
+
tag: SymbolTag = "observation",
|
|
1897
1932
|
is_hidden: bool = False,
|
|
1898
1933
|
activity: Activity | None = None,
|
|
1899
1934
|
) -> _MethodDecorator: ...
|
|
1900
1935
|
|
|
1901
1936
|
|
|
1902
1937
|
def method(
|
|
1903
|
-
fn:
|
|
1938
|
+
fn: Any = None,
|
|
1904
1939
|
*,
|
|
1905
|
-
tag:
|
|
1940
|
+
tag: SymbolTag = "observation",
|
|
1906
1941
|
is_hidden: bool = False,
|
|
1907
1942
|
activity: Activity | None = None,
|
|
1908
|
-
) ->
|
|
1943
|
+
) -> Any:
|
|
1909
1944
|
"""Describe a public method on an object exported through `Symbol`.
|
|
1910
1945
|
|
|
1911
1946
|
Args:
|
|
@@ -1950,16 +1985,13 @@ def method(
|
|
|
1950
1985
|
_activity_spec(activity)
|
|
1951
1986
|
|
|
1952
1987
|
def _mark(actual: _Method) -> _Method:
|
|
1953
|
-
declared = (
|
|
1954
|
-
actual.__func__
|
|
1955
|
-
if isinstance(actual, (staticmethod, classmethod))
|
|
1956
|
-
else actual
|
|
1957
|
-
)
|
|
1988
|
+
declared = _method_function(actual)
|
|
1958
1989
|
if not callable(declared):
|
|
1959
1990
|
raise ValueError("vis.method(...) requires a callable method")
|
|
1960
|
-
|
|
1961
|
-
declared.
|
|
1962
|
-
declared.
|
|
1991
|
+
# Pyright does not model attributes that code sets on a function.
|
|
1992
|
+
declared.__vis_symbol_tag__ = tag # pyright: ignore[reportFunctionMemberAccess]
|
|
1993
|
+
declared.__vis_symbol_hidden__ = bool(is_hidden) # pyright: ignore[reportFunctionMemberAccess]
|
|
1994
|
+
declared.__vis_symbol_activity__ = activity # pyright: ignore[reportFunctionMemberAccess]
|
|
1963
1995
|
return actual
|
|
1964
1996
|
|
|
1965
1997
|
return _mark if fn is None else _mark(fn)
|
|
@@ -1972,9 +2004,7 @@ def _public_members(obj):
|
|
|
1972
2004
|
for name, raw in vars(cls).items():
|
|
1973
2005
|
if name.startswith("_"):
|
|
1974
2006
|
continue
|
|
1975
|
-
declared = (
|
|
1976
|
-
raw.__func__ if isinstance(raw, (staticmethod, classmethod)) else raw
|
|
1977
|
-
)
|
|
2007
|
+
declared = _method_function(raw)
|
|
1978
2008
|
candidates[name] = (
|
|
1979
2009
|
"method" if inspect.isroutine(declared) else "value",
|
|
1980
2010
|
declared,
|
|
@@ -2074,7 +2104,7 @@ class Symbol:
|
|
|
2074
2104
|
|
|
2075
2105
|
fn: Callable[..., Any] | object
|
|
2076
2106
|
name: str | None = None
|
|
2077
|
-
tag:
|
|
2107
|
+
tag: SymbolTag = "observation"
|
|
2078
2108
|
is_hidden: bool = False
|
|
2079
2109
|
activity: Activity | None = None
|
|
2080
2110
|
|
|
@@ -2184,7 +2214,7 @@ class ToolSpec:
|
|
|
2184
2214
|
|
|
2185
2215
|
version: int
|
|
2186
2216
|
name: str
|
|
2187
|
-
tag:
|
|
2217
|
+
tag: SymbolTag
|
|
2188
2218
|
description: str
|
|
2189
2219
|
signature: str
|
|
2190
2220
|
parameters: tuple[ParameterSpec, ...]
|
|
@@ -2223,7 +2253,7 @@ def _catalog_type(contract):
|
|
|
2223
2253
|
)
|
|
2224
2254
|
|
|
2225
2255
|
|
|
2226
|
-
def _catalog_presentation(*, phase, result=None, error=None, **_):
|
|
2256
|
+
def _catalog_presentation(*, phase, result: Any = None, error=None, **_):
|
|
2227
2257
|
if phase == "failure":
|
|
2228
2258
|
return ActivityPresentation("Tool reference", str(error))
|
|
2229
2259
|
if phase == "start":
|
|
@@ -2565,10 +2595,11 @@ def _provider_wire(value):
|
|
|
2565
2595
|
|
|
2566
2596
|
class _ProviderValue:
|
|
2567
2597
|
__slots__ = ()
|
|
2598
|
+
__dataclass_fields__: ClassVar[dict[str, Any]] # Each subclass is a dataclass.
|
|
2568
2599
|
|
|
2569
2600
|
def to_wire(self) -> dict[str, ProviderJSON]:
|
|
2570
2601
|
"""Return fresh host data. Declared optional fields are omitted, not null."""
|
|
2571
|
-
value = {
|
|
2602
|
+
value: dict[str, ProviderJSON] = {
|
|
2572
2603
|
f.name: _provider_wire(getattr(self, f.name))
|
|
2573
2604
|
for f in fields(self)
|
|
2574
2605
|
if f.name != "extra" and getattr(self, f.name) is not None
|
|
@@ -2839,13 +2870,14 @@ def _provider_callback(name, fn):
|
|
|
2839
2870
|
raise TypeError(f"vis.Provider {name} returned an awaitable")
|
|
2840
2871
|
if result is None or name in ("logout_fn", "on_selected_fn"):
|
|
2841
2872
|
return None
|
|
2842
|
-
|
|
2873
|
+
expected_types: dict[str, type[_ProviderValue]] = {
|
|
2843
2874
|
"get_token_fn": ProviderCredential,
|
|
2844
2875
|
"detect_fn": ProviderCredential,
|
|
2845
2876
|
"refresh_token_fn": ProviderCredential,
|
|
2846
2877
|
"status_fn": ProviderStatus,
|
|
2847
2878
|
"limits_fn": ProviderLimits,
|
|
2848
|
-
}
|
|
2879
|
+
}
|
|
2880
|
+
expected = expected_types.get(name)
|
|
2849
2881
|
if expected is not None:
|
|
2850
2882
|
if not isinstance(result, expected):
|
|
2851
2883
|
raise TypeError(
|
|
@@ -3092,7 +3124,7 @@ class Shell(dict):
|
|
|
3092
3124
|
|
|
3093
3125
|
def logs(self, offset=None, limit=None):
|
|
3094
3126
|
# A NEGATIVE offset reads the last n LINES; a positive one is a byte cursor.
|
|
3095
|
-
opts = {"op": "logs"}
|
|
3127
|
+
opts: dict[str, Any] = {"op": "logs"}
|
|
3096
3128
|
if offset is not None:
|
|
3097
3129
|
opts["offset"] = int(offset)
|
|
3098
3130
|
if limit is not None:
|
|
@@ -3152,7 +3184,8 @@ class _Fs:
|
|
|
3152
3184
|
|
|
3153
3185
|
@staticmethod
|
|
3154
3186
|
def _door():
|
|
3155
|
-
|
|
3187
|
+
# Only the engine's sandbox provides this module.
|
|
3188
|
+
import _vis_fs # pyright: ignore[reportMissingImports]
|
|
3156
3189
|
|
|
3157
3190
|
return _vis_fs
|
|
3158
3191
|
|
|
@@ -3634,7 +3667,8 @@ class _Node:
|
|
|
3634
3667
|
class _KeyedNode(_Node):
|
|
3635
3668
|
"""A node holding items the extension addresses by id: it can drop them."""
|
|
3636
3669
|
|
|
3637
|
-
def remove(self, *item_ids):
|
|
3670
|
+
def remove(self: _Node, *item_ids):
|
|
3671
|
+
# Stat and Steps borrow this method, so `self` can be any node.
|
|
3638
3672
|
# Ids as arguments or as one iterable, because a caller with a list
|
|
3639
3673
|
# should not have to spread it.
|
|
3640
3674
|
ids = (
|
|
@@ -4346,13 +4380,17 @@ class _LiveRecorder:
|
|
|
4346
4380
|
yield node
|
|
4347
4381
|
yield from _LiveRecorder._nodes(node.get("fields"))
|
|
4348
4382
|
|
|
4349
|
-
def
|
|
4350
|
-
"""Return
|
|
4383
|
+
def _open_view(self):
|
|
4384
|
+
"""Return the materialized view; a test must open one first."""
|
|
4351
4385
|
if self._view is None:
|
|
4352
4386
|
raise AssertionError("no test live view is open")
|
|
4387
|
+
return self._view
|
|
4388
|
+
|
|
4389
|
+
def node(self, node_id):
|
|
4390
|
+
"""Return one materialized node by id, at any depth."""
|
|
4353
4391
|
return next(
|
|
4354
4392
|
node
|
|
4355
|
-
for node in self._nodes(self.
|
|
4393
|
+
for node in self._nodes(self._open_view().get("nodes"))
|
|
4356
4394
|
if node["id"] == node_id
|
|
4357
4395
|
)
|
|
4358
4396
|
|
|
@@ -4408,7 +4446,7 @@ class _LiveRecorder:
|
|
|
4408
4446
|
return found
|
|
4409
4447
|
return None
|
|
4410
4448
|
|
|
4411
|
-
return find(self.
|
|
4449
|
+
return find(self._open_view().get("nodes"))
|
|
4412
4450
|
|
|
4413
4451
|
def _apply(self, op):
|
|
4414
4452
|
action = op["op"]
|
|
@@ -4417,10 +4455,9 @@ class _LiveRecorder:
|
|
|
4417
4455
|
found = (
|
|
4418
4456
|
self._parent(op.get("after")) if op.get("after") is not None else None
|
|
4419
4457
|
)
|
|
4458
|
+
view = self._open_view()
|
|
4420
4459
|
siblings, at = (
|
|
4421
|
-
(
|
|
4422
|
-
if found is None
|
|
4423
|
-
else found
|
|
4460
|
+
(view["nodes"], len(view["nodes"])) if found is None else found
|
|
4424
4461
|
)
|
|
4425
4462
|
siblings.insert(at + (1 if found is not None else 0), node)
|
|
4426
4463
|
return
|
|
@@ -4537,7 +4574,7 @@ class _LiveRecorder:
|
|
|
4537
4574
|
for op in (envelope.get("patch") or {}).get("ops") or []:
|
|
4538
4575
|
self._apply(op)
|
|
4539
4576
|
self._seq += 1
|
|
4540
|
-
self.
|
|
4577
|
+
self._open_view()["seq"] = self._seq
|
|
4541
4578
|
answer = {"view_id": self.view_id, "is_open": True, "seq": self._seq}
|
|
4542
4579
|
elif action == "state":
|
|
4543
4580
|
answer = {"view_id": self.view_id, "is_open": True, "view": self._view}
|
|
@@ -4684,9 +4721,11 @@ def _assert_catalog(
|
|
|
4684
4721
|
mutation names. Test invocation, validation, IO and cancellation separately. Any and
|
|
4685
4722
|
opaque types are allowed. Unresolved annotations fail with their path.
|
|
4686
4723
|
"""
|
|
4724
|
+
roots = catalog.spec()
|
|
4725
|
+
assert isinstance(roots, tuple), "Catalog.spec() lists the top-level specs"
|
|
4687
4726
|
tools = tuple(
|
|
4688
4727
|
tool
|
|
4689
|
-
for spec in
|
|
4728
|
+
for spec in roots
|
|
4690
4729
|
for tool in (spec.members if isinstance(spec, NamespaceSpec) else (spec,))
|
|
4691
4730
|
)
|
|
4692
4731
|
actual_names = [tool.name for tool in tools]
|
{vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/src/blockether/vis/extension_package.py
RENAMED
|
@@ -18,6 +18,7 @@ import tomllib
|
|
|
18
18
|
import uuid
|
|
19
19
|
from http.client import HTTPException
|
|
20
20
|
from pathlib import Path
|
|
21
|
+
from typing import TYPE_CHECKING
|
|
21
22
|
from urllib.error import HTTPError, URLError
|
|
22
23
|
from urllib.parse import unquote, urlsplit
|
|
23
24
|
from urllib.request import HTTPRedirectHandler, Request, build_opener
|
|
@@ -27,7 +28,9 @@ try:
|
|
|
27
28
|
from packaging.specifiers import SpecifierSet
|
|
28
29
|
from packaging.utils import canonicalize_name
|
|
29
30
|
from packaging.version import Version
|
|
30
|
-
except ImportError:
|
|
31
|
+
except ImportError: # The engine's Python can lack packaging; pip vendors a copy.
|
|
32
|
+
if TYPE_CHECKING: # Type checkers see only the declared packaging dependency.
|
|
33
|
+
raise
|
|
31
34
|
from pip._vendor.packaging.requirements import Requirement
|
|
32
35
|
from pip._vendor.packaging.specifiers import SpecifierSet
|
|
33
36
|
from pip._vendor.packaging.utils import canonicalize_name
|
|
@@ -961,6 +964,7 @@ def update(
|
|
|
961
964
|
if subdirectory is None and not folder:
|
|
962
965
|
folder = None
|
|
963
966
|
active, current = _installed_repository(directory, repository, folder)
|
|
967
|
+
assert active is not None and current is not None # A required lookup raises.
|
|
964
968
|
name = current["name"]
|
|
965
969
|
release = _select(
|
|
966
970
|
_releases(current["repository_url"], current["subdirectory"]), version
|
|
@@ -1011,6 +1015,7 @@ def rollback(
|
|
|
1011
1015
|
if subdirectory is None and not folder:
|
|
1012
1016
|
folder = None
|
|
1013
1017
|
active, current = _installed_repository(directory, repository, folder)
|
|
1018
|
+
assert active is not None and current is not None # A required lookup raises.
|
|
1014
1019
|
name = current["name"]
|
|
1015
1020
|
if version is not None:
|
|
1016
1021
|
release = _select(
|
|
@@ -1356,8 +1361,7 @@ def sync(
|
|
|
1356
1361
|
if not dry_run:
|
|
1357
1362
|
directory.mkdir(parents=True, exist_ok=True)
|
|
1358
1363
|
lock = directory / ".sync-lock"
|
|
1359
|
-
if
|
|
1360
|
-
fd = _acquire(lock)
|
|
1364
|
+
fd = None if dry_run else _acquire(lock)
|
|
1361
1365
|
try:
|
|
1362
1366
|
records = _sync_records(directory)
|
|
1363
1367
|
results = []
|
|
@@ -1381,7 +1385,8 @@ def sync(
|
|
|
1381
1385
|
_save_sync_records(directory, records)
|
|
1382
1386
|
elif dry_run:
|
|
1383
1387
|
unchanged = (
|
|
1384
|
-
|
|
1388
|
+
current is not None
|
|
1389
|
+
and _sync_owned(destination, current)
|
|
1385
1390
|
and current["spec"] == spec
|
|
1386
1391
|
and not refresh
|
|
1387
1392
|
and not _tracks_latest(spec)
|
|
@@ -375,7 +375,7 @@ class RoomsClient:
|
|
|
375
375
|
*,
|
|
376
376
|
lease_seconds: int | None = None,
|
|
377
377
|
) -> int:
|
|
378
|
-
body = {"sessions": sessions}
|
|
378
|
+
body: dict[str, Any] = {"sessions": sessions}
|
|
379
379
|
if lease_seconds is not None:
|
|
380
380
|
body["lease_seconds"] = lease_seconds
|
|
381
381
|
return self._request("POST", self._path(room_id, "/presence"), body)[
|
|
@@ -177,6 +177,7 @@ class ViewEvent:
|
|
|
177
177
|
"view.close": "close_event",
|
|
178
178
|
}[event_type]
|
|
179
179
|
validate("view", definition, value)
|
|
180
|
+
payload: dict[str, Any]
|
|
180
181
|
if event_type == "view.open":
|
|
181
182
|
model = InputView if value["kind"] == "input" else LiveView
|
|
182
183
|
document = model.from_wire(value["view"])
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Regression coverage for SDK types under Pyright standard checks."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import blockether.vis.extension as vis
|
|
9
|
+
|
|
10
|
+
_SYMBOL_TAG_FIXTURE = """from typing import assert_type
|
|
11
|
+
|
|
12
|
+
import blockether.vis.extension as vis
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _knows(tag: vis.SymbolTag) -> bool:
|
|
16
|
+
try:
|
|
17
|
+
vis.method(tag=tag)
|
|
18
|
+
except ValueError:
|
|
19
|
+
return False
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
CHECK: vis.SymbolTag = "verification" if _knows("verification") else "observation"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def bind(fn, *, tag: vis.SymbolTag = "observation"):
|
|
27
|
+
return vis.method(tag=tag)(fn)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
bind(print, tag=CHECK)
|
|
31
|
+
assert_type(vis.Symbol(print, tag=CHECK).tag, vis.SymbolTag)
|
|
32
|
+
UNKNOWN: vis.SymbolTag = "unknown"
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
_SETTING_FIXTURE = """from typing import assert_type
|
|
36
|
+
|
|
37
|
+
import blockether.vis.extension as vis
|
|
38
|
+
|
|
39
|
+
ENABLED = vis.Setting(id="enabled", label="Desktop alerts", default=True)
|
|
40
|
+
LEVEL = vis.Setting(
|
|
41
|
+
id="level", label="Level", default="fast", type="enum", choices=["fast", "slow"]
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def start(*, enabled: bool) -> None: ...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def context(env):
|
|
49
|
+
start(enabled=ENABLED.value())
|
|
50
|
+
assert_type(ENABLED, vis.Setting[bool])
|
|
51
|
+
assert_type(ENABLED.value(), bool)
|
|
52
|
+
assert_type(LEVEL.value(), str)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _pyright(tmp_path, *targets):
|
|
57
|
+
config = tmp_path / "pyrightconfig.json"
|
|
58
|
+
config.write_text(
|
|
59
|
+
json.dumps({"typeCheckingMode": "standard", "pythonVersion": "3.11"})
|
|
60
|
+
)
|
|
61
|
+
command = [
|
|
62
|
+
sys.executable,
|
|
63
|
+
"-m",
|
|
64
|
+
"pyright",
|
|
65
|
+
"--project",
|
|
66
|
+
str(config),
|
|
67
|
+
"--pythonpath",
|
|
68
|
+
sys.executable,
|
|
69
|
+
"--warnings",
|
|
70
|
+
"--outputjson",
|
|
71
|
+
*(str(target) for target in targets),
|
|
72
|
+
]
|
|
73
|
+
return subprocess.run(command, capture_output=True, text=True, check=False)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_sdk_sources_pass_pyright_standard(tmp_path):
|
|
77
|
+
# The SDK ships py.typed, so type checkers trust its annotations. Each
|
|
78
|
+
# standard-mode finding needs a fix or an explicit suppression (#315).
|
|
79
|
+
package = Path(vis.__file__).resolve().parent
|
|
80
|
+
result = _pyright(tmp_path, package)
|
|
81
|
+
assert result.returncode == 0, result.stdout + result.stderr
|
|
82
|
+
summary = json.loads(result.stdout)["summary"]
|
|
83
|
+
assert summary["filesAnalyzed"] == len(list(package.rglob("*.py")))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_symbol_tag_types_tags_in_variables(tmp_path):
|
|
87
|
+
# A tag in a variable or a helper parameter needs a public type (#313).
|
|
88
|
+
# The alias still rejects a tag that Vis does not know.
|
|
89
|
+
fixture = tmp_path / "symbol_tag.py"
|
|
90
|
+
fixture.write_text(_SYMBOL_TAG_FIXTURE)
|
|
91
|
+
result = _pyright(tmp_path, fixture)
|
|
92
|
+
report = json.loads(result.stdout)
|
|
93
|
+
found = [
|
|
94
|
+
(item["range"]["start"]["line"], item["rule"])
|
|
95
|
+
for item in report["generalDiagnostics"]
|
|
96
|
+
]
|
|
97
|
+
unknown = _SYMBOL_TAG_FIXTURE.splitlines().index(
|
|
98
|
+
'UNKNOWN: vis.SymbolTag = "unknown"'
|
|
99
|
+
)
|
|
100
|
+
assert found == [(unknown, "reportAssignmentType")], result.stdout
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_setting_value_has_the_type_of_its_default(tmp_path):
|
|
104
|
+
# A boolean setting reads a bool and a choice setting reads a str (#314).
|
|
105
|
+
fixture = tmp_path / "setting_value.py"
|
|
106
|
+
fixture.write_text(_SETTING_FIXTURE)
|
|
107
|
+
result = _pyright(tmp_path, fixture)
|
|
108
|
+
assert result.returncode == 0, result.stdout + result.stderr
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-arguments.json
RENAMED
|
File without changes
|
{vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-cases.json
RENAMED
|
File without changes
|
|
File without changes
|
{vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-execution.json
RENAMED
|
File without changes
|
{vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-groups.json
RENAMED
|
File without changes
|
{vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-read-session.json
RENAMED
|
File without changes
|
{vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-reads.json
RENAMED
|
File without changes
|
{vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-results.json
RENAMED
|
File without changes
|
{vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/activity-tables.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/contracts/fixtures/live-primitives.json
RENAMED
|
File without changes
|
{vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/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
|
|
File without changes
|
|
File without changes
|
{vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/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
|
{vis_agent-0.2.32.dev7313 → vis_agent-0.2.32.dev7344}/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.32.dev7313 → vis_agent-0.2.32.dev7344}/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.32.dev7313 → vis_agent-0.2.32.dev7344}/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
|