vis-agent 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vis_agent-0.2.2 → vis_agent-0.2.3}/PKG-INFO +8 -3
- vis_agent-0.2.3/README.md +16 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/pyproject.toml +1 -1
- {vis_agent-0.2.2 → vis_agent-0.2.3}/src/blockether/vis/engine/__init__.py +6 -3
- vis_agent-0.2.3/src/blockether/vis/engine/_agent.py +105 -0
- vis_agent-0.2.3/tests/test_agent.py +216 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_engine.py +3 -0
- vis_agent-0.2.3/tests/test_sdk_guide.py +614 -0
- vis_agent-0.2.2/README.md +0 -11
- {vis_agent-0.2.2 → vis_agent-0.2.3}/.gitignore +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/LICENSE +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/activity.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/config.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/content.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/council.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/fixtures/activity-arguments.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/fixtures/activity-cases.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/fixtures/activity-copy.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/fixtures/activity-groups.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/fixtures/activity-repl.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/fixtures/activity-results.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/fixtures/activity-tables.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/fixtures/activity.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/fixtures/live-primitives.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/fixtures/session-health.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/fixtures/view.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/gateway.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/provider.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/python-host.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/schema/activity.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/schema/common.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/schema/config.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/schema/content.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/schema/council.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/schema/gateway.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/schema/provider.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/schema/python-host.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/schema/surface.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/schema/symbol.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/schema/test-runner.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/schema/toggle.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/schema/view.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/surface.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/symbol.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/test-runner.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/toggle.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/contracts/view.json +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/examples/greeter/tests/test_greeter.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/hatch_build.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/src/blockether/vis/__init__.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/src/blockether/vis/_contracts.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/src/blockether/vis/_outside.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/src/blockether/vis/_wire.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/src/blockether/vis/activity.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/src/blockether/vis/engine/_client.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/src/blockether/vis/engine/_council.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/src/blockether/vis/engine/_local.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/src/blockether/vis/extension.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/src/blockether/vis/extension_package.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/src/blockether/vis/py.typed +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/src/blockether/vis/views.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/conftest.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_activity.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_authoring.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_catalog.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_catalog_recipe.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_ci_report_recipe.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_client.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_code_quality_recipe.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_contracts.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_council.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_declarations.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_extension_package.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_goals.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_live_primitives.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_live_wait.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_local.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_monitor_recipe.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_namespace.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_outside.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_providers.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_registration.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_symbol_contract.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_testing.py +0 -0
- {vis_agent-0.2.2 → vis_agent-0.2.3}/tests/test_views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vis-agent
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: The Vis Python SDK — author extensions and control local or remote engines.
|
|
5
5
|
Project-URL: Homepage, https://vis.blockether.com
|
|
6
6
|
Project-URL: Documentation, https://vis.blockether.com
|
|
@@ -28,6 +28,11 @@ Python SDK for Vis extensions and agent sessions.
|
|
|
28
28
|
pip install vis-agent
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Requires Python 3.11+.
|
|
31
|
+
Requires Python 3.11+. Install the Vis executable and configure a provider
|
|
32
|
+
separately for local use.
|
|
32
33
|
|
|
33
|
-
[
|
|
34
|
+
- [Python SDK guide](https://vis.blockether.com/python-sdk.html) — run a local
|
|
35
|
+
agent, continue a conversation or connect to a shared gateway. The new
|
|
36
|
+
`Agent(project=".")` API requires the source SDK, not PyPI `0.2.2`.
|
|
37
|
+
- [Extension guide](https://vis.blockether.com/extending.html) — add tools to Vis.
|
|
38
|
+
- [Documentation](https://vis.blockether.com) — browse all guides and reference pages.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# vis-agent
|
|
2
|
+
|
|
3
|
+
Python SDK for Vis extensions and agent sessions.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pip install vis-agent
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Requires Python 3.11+. Install the Vis executable and configure a provider
|
|
10
|
+
separately for local use.
|
|
11
|
+
|
|
12
|
+
- [Python SDK guide](https://vis.blockether.com/python-sdk.html) — run a local
|
|
13
|
+
agent, continue a conversation or connect to a shared gateway. The new
|
|
14
|
+
`Agent(project=".")` API requires the source SDK, not PyPI `0.2.2`.
|
|
15
|
+
- [Extension guide](https://vis.blockether.com/extending.html) — add tools to Vis.
|
|
16
|
+
- [Documentation](https://vis.blockether.com) — browse all guides and reference pages.
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Vis agents, engine clients, lifecycle records, streams and transport errors.
|
|
2
2
|
|
|
3
|
-
``
|
|
4
|
-
|
|
3
|
+
``Agent`` runs one conversation locally or through an existing gateway.
|
|
4
|
+
``GatewayClient`` manages gateway sessions; ``LocalEngine`` owns a local stdio
|
|
5
|
+
process. Importing these starts nothing and never binds an extension host.
|
|
5
6
|
"""
|
|
6
7
|
|
|
8
|
+
from ._agent import Agent
|
|
7
9
|
from ._client import (
|
|
8
10
|
Event,
|
|
9
11
|
Events,
|
|
@@ -33,6 +35,7 @@ from ._council import (
|
|
|
33
35
|
from ._local import LocalEngine
|
|
34
36
|
|
|
35
37
|
__all__ = [
|
|
38
|
+
"Agent",
|
|
36
39
|
"Council",
|
|
37
40
|
"CouncilEntry",
|
|
38
41
|
"CouncilKind",
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""One conversation using an owned stdio engine or an existing HTTP gateway."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path, PurePosixPath
|
|
4
|
+
|
|
5
|
+
from ._client import GatewayClient, Session, TransportError, Turn
|
|
6
|
+
from ._local import LocalEngine
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Agent:
|
|
10
|
+
"""Run tasks locally, or connect to a separately running gateway.
|
|
11
|
+
|
|
12
|
+
Agent() resolves the existing local project directory (default: ".") at
|
|
13
|
+
construction. Context entry, send(), run() or session starts a private
|
|
14
|
+
LocalEngine and one conversation, without an HTTP listener. close() stops
|
|
15
|
+
that process and discards its temporary session database, not file edits.
|
|
16
|
+
Environment, provider credentials and extensions are inherited. Linux/macOS.
|
|
17
|
+
|
|
18
|
+
With gateway_url, project must be an absolute POSIX path on that gateway;
|
|
19
|
+
it is never resolved or checked on the client machine. No local engine is
|
|
20
|
+
needed. The agent creates one app-channel session through GatewayClient.
|
|
21
|
+
close() releases the client lease, not the saved session or remote work.
|
|
22
|
+
The URL and optional bearer token are explicit; no environment discovery.
|
|
23
|
+
|
|
24
|
+
executable (launcher path or argv) and startup_timeout are local-only.
|
|
25
|
+
timeout bounds transport operations in either mode. Instances use one
|
|
26
|
+
calling thread. Use GatewayClient directly to resume or manage sessions.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
project=".",
|
|
32
|
+
*,
|
|
33
|
+
gateway_url=None,
|
|
34
|
+
token=None,
|
|
35
|
+
executable="vis-agent",
|
|
36
|
+
timeout=30,
|
|
37
|
+
startup_timeout=120,
|
|
38
|
+
):
|
|
39
|
+
if gateway_url is None:
|
|
40
|
+
if token is not None:
|
|
41
|
+
raise ValueError("token requires gateway_url")
|
|
42
|
+
self.project = Path(project).resolve(strict=True)
|
|
43
|
+
if not self.project.is_dir():
|
|
44
|
+
raise NotADirectoryError(str(self.project))
|
|
45
|
+
self._client = LocalEngine(
|
|
46
|
+
executable=executable,
|
|
47
|
+
root=self.project,
|
|
48
|
+
timeout=timeout,
|
|
49
|
+
startup_timeout=startup_timeout,
|
|
50
|
+
)
|
|
51
|
+
self._session_options = {"root": str(self.project)}
|
|
52
|
+
else:
|
|
53
|
+
if executable != "vis-agent" or startup_timeout != 120:
|
|
54
|
+
raise ValueError("executable and startup_timeout are local-only")
|
|
55
|
+
self.project = PurePosixPath(project)
|
|
56
|
+
if not self.project.is_absolute():
|
|
57
|
+
raise ValueError("project must be an absolute path on the gateway")
|
|
58
|
+
self._client = GatewayClient(gateway_url, token=token, timeout=timeout)
|
|
59
|
+
self._session_options = {"root": str(self.project), "channel": "app"}
|
|
60
|
+
self._session = None
|
|
61
|
+
self._closed = False
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def session(self) -> Session:
|
|
65
|
+
"""The same conversation for all requests; connects on first access."""
|
|
66
|
+
if self._closed:
|
|
67
|
+
raise TransportError("agent is closed")
|
|
68
|
+
if self._session is None:
|
|
69
|
+
try:
|
|
70
|
+
self._client.connect()
|
|
71
|
+
self._session = self._client.create_session(**self._session_options)
|
|
72
|
+
except BaseException:
|
|
73
|
+
self.close()
|
|
74
|
+
raise
|
|
75
|
+
return self._session
|
|
76
|
+
|
|
77
|
+
def send(self, request: str, **options) -> Turn:
|
|
78
|
+
"""Submit a request, returning a Turn for progress, waiting or cancellation.
|
|
79
|
+
|
|
80
|
+
Options are passed to Session.send(), including provider, model and
|
|
81
|
+
idempotency_key. Follow-up requests use this agent's existing session.
|
|
82
|
+
"""
|
|
83
|
+
return self.session.send(request, **options)
|
|
84
|
+
|
|
85
|
+
def run(self, request: str, *, timeout=300, **options):
|
|
86
|
+
"""Submit and wait, returning the canonical turn record, not a text string.
|
|
87
|
+
|
|
88
|
+
Inspect status: failed, cancelled or suspended records are returned too.
|
|
89
|
+
A wait timeout raises VisTimeout without cancelling the turn. Context
|
|
90
|
+
exit stops unfinished local work; remote work can outlive the client.
|
|
91
|
+
"""
|
|
92
|
+
return self.send(request, **options).wait(timeout=timeout)
|
|
93
|
+
|
|
94
|
+
def close(self):
|
|
95
|
+
"""Close the owned process or gateway client, never a remote session or service."""
|
|
96
|
+
if not self._closed:
|
|
97
|
+
self._closed = True
|
|
98
|
+
self._client.close()
|
|
99
|
+
|
|
100
|
+
def __enter__(self):
|
|
101
|
+
_ = self.session
|
|
102
|
+
return self
|
|
103
|
+
|
|
104
|
+
def __exit__(self, *_):
|
|
105
|
+
self.close()
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Agent lifecycle and delegation; real transports live in test_sdk_guide."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import Mock
|
|
4
|
+
|
|
5
|
+
import blockether.vis.engine as engine
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture
|
|
10
|
+
def local(monkeypatch):
|
|
11
|
+
factory = Mock()
|
|
12
|
+
monkeypatch.setattr("blockether.vis.engine._agent.LocalEngine", factory)
|
|
13
|
+
return factory
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_default_project_is_resolved_before_chdir(local, monkeypatch, tmp_path):
|
|
17
|
+
monkeypatch.chdir(tmp_path)
|
|
18
|
+
agent = engine.Agent()
|
|
19
|
+
monkeypatch.chdir(tmp_path.parent)
|
|
20
|
+
assert agent.project == tmp_path.resolve()
|
|
21
|
+
local.assert_called_once_with(
|
|
22
|
+
executable="vis-agent", root=tmp_path.resolve(), timeout=30, startup_timeout=120
|
|
23
|
+
)
|
|
24
|
+
local.return_value.connect.assert_not_called()
|
|
25
|
+
with agent:
|
|
26
|
+
assert agent.session is local.return_value.create_session.return_value
|
|
27
|
+
local.return_value.create_session.assert_called_once_with(
|
|
28
|
+
root=str(tmp_path.resolve())
|
|
29
|
+
)
|
|
30
|
+
local.return_value.close.assert_called_once_with()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_followups_and_options_use_one_conversation(local, tmp_path):
|
|
34
|
+
with engine.Agent(tmp_path, executable=["custom-vis", "--jvm"]) as agent:
|
|
35
|
+
conversation = agent.session
|
|
36
|
+
result = agent.run("First", timeout=42, provider="provider", model="model")
|
|
37
|
+
conversation.send.assert_called_once_with(
|
|
38
|
+
"First", provider="provider", model="model"
|
|
39
|
+
)
|
|
40
|
+
conversation.send.return_value.wait.assert_called_once_with(timeout=42)
|
|
41
|
+
assert result is conversation.send.return_value.wait.return_value
|
|
42
|
+
turn = agent.send("Follow up", idempotency_key="same-request")
|
|
43
|
+
assert turn is conversation.send.return_value
|
|
44
|
+
conversation.send.assert_called_with(
|
|
45
|
+
"Follow up", idempotency_key="same-request"
|
|
46
|
+
)
|
|
47
|
+
local.return_value.connect.assert_called_once_with()
|
|
48
|
+
local.return_value.create_session.assert_called_once()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.parametrize("status", ["completed", "failed", "cancelled", "suspended"])
|
|
52
|
+
def test_run_preserves_the_canonical_turn_record(local, tmp_path, status):
|
|
53
|
+
result = {"status": status, "content": [], "turn_id": "turn"}
|
|
54
|
+
local.return_value.create_session.return_value.send.return_value.wait.return_value = result
|
|
55
|
+
with engine.Agent(tmp_path) as agent:
|
|
56
|
+
assert agent.run("Request") is result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.parametrize("operation", ["connect", "create_session"])
|
|
60
|
+
def test_startup_failure_closes_engine_and_prevents_reuse(local, tmp_path, operation):
|
|
61
|
+
getattr(local.return_value, operation).side_effect = engine.TransportError(
|
|
62
|
+
"startup"
|
|
63
|
+
)
|
|
64
|
+
agent = engine.Agent(tmp_path)
|
|
65
|
+
with pytest.raises(engine.TransportError, match="startup"):
|
|
66
|
+
with agent:
|
|
67
|
+
pytest.fail("startup should not succeed")
|
|
68
|
+
local.return_value.close.assert_called_once_with()
|
|
69
|
+
with pytest.raises(engine.TransportError, match="closed"):
|
|
70
|
+
agent.send("retry")
|
|
71
|
+
agent.close()
|
|
72
|
+
local.return_value.close.assert_called_once_with()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_close_without_start_and_body_exception(local, tmp_path):
|
|
76
|
+
agent = engine.Agent(tmp_path)
|
|
77
|
+
agent.close()
|
|
78
|
+
agent.close()
|
|
79
|
+
local.return_value.connect.assert_not_called()
|
|
80
|
+
local.return_value.close.assert_called_once_with()
|
|
81
|
+
with pytest.raises(engine.TransportError, match="closed"):
|
|
82
|
+
with agent:
|
|
83
|
+
pass
|
|
84
|
+
local.reset_mock()
|
|
85
|
+
with pytest.raises(RuntimeError, match="body"):
|
|
86
|
+
with engine.Agent(tmp_path):
|
|
87
|
+
raise RuntimeError("body")
|
|
88
|
+
local.return_value.close.assert_called_once_with()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_wait_timeout_does_not_cancel_the_turn(local, tmp_path):
|
|
92
|
+
with engine.Agent(tmp_path) as agent:
|
|
93
|
+
turn = agent.session.send.return_value
|
|
94
|
+
turn.wait.side_effect = engine.VisTimeout("deadline")
|
|
95
|
+
with pytest.raises(engine.VisTimeout, match="deadline"):
|
|
96
|
+
agent.run("Request", timeout=0.1)
|
|
97
|
+
turn.cancel.assert_not_called()
|
|
98
|
+
local.return_value.close.assert_not_called()
|
|
99
|
+
local.return_value.close.assert_called_once_with()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_project_must_be_an_existing_directory(tmp_path):
|
|
103
|
+
with pytest.raises(FileNotFoundError):
|
|
104
|
+
engine.Agent(tmp_path / "missing")
|
|
105
|
+
file = tmp_path / "file"
|
|
106
|
+
file.touch()
|
|
107
|
+
with pytest.raises(NotADirectoryError):
|
|
108
|
+
engine.Agent(file)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_missing_launcher_has_no_surviving_process(tmp_path):
|
|
112
|
+
agent = engine.Agent(tmp_path, executable=str(tmp_path / "missing"))
|
|
113
|
+
with pytest.raises(engine.TransportError):
|
|
114
|
+
with agent:
|
|
115
|
+
pass
|
|
116
|
+
agent.close()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@pytest.fixture
|
|
120
|
+
def remote(monkeypatch):
|
|
121
|
+
factory = Mock()
|
|
122
|
+
monkeypatch.setattr("blockether.vis.engine._agent.GatewayClient", factory)
|
|
123
|
+
return factory
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_remote_agent_needs_no_local_project_or_engine(local, remote):
|
|
127
|
+
agent = engine.Agent(
|
|
128
|
+
"/srv/vis-project",
|
|
129
|
+
gateway_url="https://gateway.example.com",
|
|
130
|
+
token="test-token",
|
|
131
|
+
timeout=12,
|
|
132
|
+
)
|
|
133
|
+
assert str(agent.project) == "/srv/vis-project"
|
|
134
|
+
local.assert_not_called()
|
|
135
|
+
remote.assert_called_once_with(
|
|
136
|
+
"https://gateway.example.com", token="test-token", timeout=12
|
|
137
|
+
)
|
|
138
|
+
remote.return_value.connect.assert_not_called()
|
|
139
|
+
with agent:
|
|
140
|
+
conversation = agent.session
|
|
141
|
+
remote.return_value.create_session.assert_called_once_with(
|
|
142
|
+
root="/srv/vis-project", channel="app"
|
|
143
|
+
)
|
|
144
|
+
result = agent.run("First", timeout=42, provider="provider", model="model")
|
|
145
|
+
assert result is conversation.send.return_value.wait.return_value
|
|
146
|
+
conversation.send.assert_called_once_with(
|
|
147
|
+
"First", provider="provider", model="model"
|
|
148
|
+
)
|
|
149
|
+
conversation.send.return_value.wait.assert_called_once_with(timeout=42)
|
|
150
|
+
assert agent.send("Follow up") is conversation.send.return_value
|
|
151
|
+
conversation.send.assert_called_with("Follow up")
|
|
152
|
+
remote.return_value.connect.assert_called_once_with()
|
|
153
|
+
remote.return_value.create_session.assert_called_once()
|
|
154
|
+
remote.return_value.close.assert_called_once_with()
|
|
155
|
+
conversation.delete.assert_not_called()
|
|
156
|
+
conversation.send.return_value.cancel.assert_not_called()
|
|
157
|
+
agent.close()
|
|
158
|
+
remote.return_value.close.assert_called_once_with()
|
|
159
|
+
with pytest.raises(engine.TransportError, match="closed"):
|
|
160
|
+
agent.send("Retry")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@pytest.mark.parametrize("operation", ["connect", "create_session"])
|
|
164
|
+
def test_remote_startup_failure_releases_client(remote, operation):
|
|
165
|
+
getattr(remote.return_value, operation).side_effect = engine.TransportError(
|
|
166
|
+
"startup"
|
|
167
|
+
)
|
|
168
|
+
agent = engine.Agent("/srv/vis-project", gateway_url="https://gateway.example.com")
|
|
169
|
+
with pytest.raises(engine.TransportError, match="startup"):
|
|
170
|
+
with agent:
|
|
171
|
+
pytest.fail("startup should not succeed")
|
|
172
|
+
remote.return_value.close.assert_called_once_with()
|
|
173
|
+
with pytest.raises(engine.TransportError, match="closed"):
|
|
174
|
+
agent.run("Retry")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_remote_wait_timeout_and_context_error_only_close_client(remote):
|
|
178
|
+
with pytest.raises(engine.VisTimeout, match="deadline"):
|
|
179
|
+
with engine.Agent(
|
|
180
|
+
"/srv/vis-project", gateway_url="https://gateway.example.com"
|
|
181
|
+
) as agent:
|
|
182
|
+
turn = agent.session.send.return_value
|
|
183
|
+
turn.wait.side_effect = engine.VisTimeout("deadline")
|
|
184
|
+
agent.run("Request", timeout=0.1)
|
|
185
|
+
turn.cancel.assert_not_called()
|
|
186
|
+
remote.return_value.close.assert_called_once_with()
|
|
187
|
+
remote.return_value.create_session.return_value.delete.assert_not_called()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_remote_close_before_connect(remote):
|
|
191
|
+
agent = engine.Agent("/srv/vis-project", gateway_url="https://gateway.example.com")
|
|
192
|
+
agent.close()
|
|
193
|
+
agent.close()
|
|
194
|
+
remote.return_value.connect.assert_not_called()
|
|
195
|
+
remote.return_value.close.assert_called_once_with()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@pytest.mark.parametrize("project", [".", "relative/project", "", "~/project"])
|
|
199
|
+
def test_remote_project_must_be_explicit_absolute_path(local, project):
|
|
200
|
+
with pytest.raises(ValueError, match="absolute.*gateway"):
|
|
201
|
+
engine.Agent(project, gateway_url="https://gateway.example.com")
|
|
202
|
+
local.assert_not_called()
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@pytest.mark.parametrize(
|
|
206
|
+
"options",
|
|
207
|
+
[
|
|
208
|
+
{"token": "test-token"},
|
|
209
|
+
{"gateway_url": "https://gateway.example.com", "executable": "custom-vis"},
|
|
210
|
+
{"gateway_url": "https://gateway.example.com", "startup_timeout": 10},
|
|
211
|
+
],
|
|
212
|
+
)
|
|
213
|
+
def test_mixed_transport_options_are_rejected(local, tmp_path, options):
|
|
214
|
+
with pytest.raises(ValueError):
|
|
215
|
+
engine.Agent(tmp_path, **options)
|
|
216
|
+
local.assert_not_called()
|
|
@@ -247,6 +247,7 @@ def sdk_fixture(
|
|
|
247
247
|
council=False,
|
|
248
248
|
tool_code=None,
|
|
249
249
|
before_reply=None,
|
|
250
|
+
project_extensions=None,
|
|
250
251
|
):
|
|
251
252
|
raw = os.environ.get("VIS_TEST_LOCAL_COMMAND")
|
|
252
253
|
if not raw:
|
|
@@ -258,6 +259,8 @@ def sdk_fixture(
|
|
|
258
259
|
extensions = work / ".vis" / "extensions"
|
|
259
260
|
extensions.mkdir(parents=True)
|
|
260
261
|
(extensions / "fixture.py").write_text(EXTENSION)
|
|
262
|
+
for name, source in (project_extensions or {}).items():
|
|
263
|
+
(extensions / name).write_text(source)
|
|
261
264
|
monkeypatch.setenv("HOME", str(home))
|
|
262
265
|
monkeypatch.setenv("JAVA_TOOL_OPTIONS", f"-Duser.home={home}")
|
|
263
266
|
monkeypatch.delenv("VIS_GATEWAY_URL", raising=False)
|