virtuai-cli 1.0.0__tar.gz → 1.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/PKG-INFO +1 -1
  2. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/pyproject.toml +1 -1
  3. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/__init__.py +1 -1
  4. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/pull.py +19 -6
  5. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli.egg-info/PKG-INFO +1 -1
  6. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli.egg-info/SOURCES.txt +1 -0
  7. virtuai_cli-1.0.2/tests/test_pull_fallback.py +79 -0
  8. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/README.md +0 -0
  9. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/setup.cfg +0 -0
  10. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/browser.py +0 -0
  11. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/chat/__init__.py +0 -0
  12. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/chat/agents.py +0 -0
  13. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/chat/ask.py +0 -0
  14. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/chat/command.py +0 -0
  15. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/chat/history.py +0 -0
  16. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/chat/sse.py +0 -0
  17. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/chat/tui.py +0 -0
  18. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/chat/widgets.py +0 -0
  19. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/config.py +0 -0
  20. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/executor.py +0 -0
  21. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/main.py +0 -0
  22. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/opencode.py +0 -0
  23. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/runner.py +0 -0
  24. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli/security.py +0 -0
  25. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli.egg-info/dependency_links.txt +0 -0
  26. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli.egg-info/entry_points.txt +0 -0
  27. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli.egg-info/requires.txt +0 -0
  28. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/src/virtuai_cli.egg-info/top_level.txt +0 -0
  29. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/tests/test_browser.py +0 -0
  30. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/tests/test_frame_concurrency.py +0 -0
  31. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/tests/test_process_group_kill.py +0 -0
  32. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/tests/test_run_single_instance.py +0 -0
  33. {virtuai_cli-1.0.0 → virtuai_cli-1.0.2}/tests/test_session_isolation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: virtuai-cli
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Run VirtuAI deep agents on your local machine
5
5
  Author-email: uCloudStore <lmoreno@ucloudstore.com>
6
6
  License: Proprietary
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "virtuai-cli"
7
- version = "1.0.0"
7
+ version = "1.0.2"
8
8
  description = "Run VirtuAI deep agents on your local machine"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,2 +1,2 @@
1
1
  """VirtuAI local CLI."""
2
- __version__ = "1.0.0"
2
+ __version__ = "1.0.2"
@@ -35,7 +35,10 @@ logger = logging.getLogger(__name__)
35
35
  _HEARTBEAT_SECONDS = 5.0
36
36
  #: Output is sent this often while there is some, so a stream reads as one.
37
37
  _FLUSH_SECONDS = 0.25
38
- _RETRY_DELAYS = (1, 2, 5, 10, 20, 30)
38
+ #: A poll is cheap, and the only thing waiting behind a long backoff is the
39
+ #: agent: capped at 5 s. With (…, 10, 20, 30) a runner came back ~20 s after
40
+ #: the server did on a dev rollout.
41
+ _RETRY_DELAYS = (1, 2, 3, 5)
39
42
  #: How long a finished job keeps trying to hand in its result.
40
43
  _RESULT_RETRY_SECONDS = 120.0
41
44
  #: Past this many jobs in hand, wait for some to finish before asking for more.
@@ -142,12 +145,20 @@ class _Session:
142
145
  "ready": await runner._ready_payload(self.workdir),
143
146
  }
144
147
  resp = await self.http.post("/api/cli/v2/hello", json=body)
145
- if resp.status_code == 404:
146
- raise PullUnavailable()
147
148
  if resp.status_code == 401:
148
149
  raise TokenRejected("the server rejected this runner's token; pair it again")
149
- resp.raise_for_status()
150
- data = resp.json()
150
+ if resp.status_code >= 500:
151
+ resp.raise_for_status() # a server in trouble: retried
152
+ # Anything else that is not a v2 hello means this server does not
153
+ # offer pull. Not only 404: a server from before v2 answers a POST
154
+ # here with 405 (its frontend's catch-all route), and some proxies
155
+ # with an HTML page. 1.0.0 retried those forever and never connected.
156
+ try:
157
+ data = resp.json() if resp.status_code == 200 else None
158
+ except ValueError:
159
+ data = None
160
+ if not isinstance(data, dict) or not data.get("runner_id"):
161
+ raise PullUnavailable()
151
162
  self.runner_id = data["runner_id"]
152
163
  self.wait = int(data.get("wait_seconds", self.wait))
153
164
  self.heartbeat = float(data.get("heartbeat_seconds", self.heartbeat))
@@ -192,7 +203,9 @@ class _Session:
192
203
  continue
193
204
  if resp.status_code == 401:
194
205
  raise TokenRejected("the server rejected this runner's token; pair it again")
195
- if resp.status_code == 404:
206
+ if resp.status_code in (404, 405) or (
207
+ resp.status_code == 200
208
+ and not resp.headers.get("content-type", "").startswith("application/json")):
196
209
  not_found += 1
197
210
  if not_found >= _GONE_AFTER_404S:
198
211
  raise PullUnavailable()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: virtuai-cli
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Run VirtuAI deep agents on your local machine
5
5
  Author-email: uCloudStore <lmoreno@ucloudstore.com>
6
6
  License: Proprietary
@@ -26,5 +26,6 @@ src/virtuai_cli/chat/widgets.py
26
26
  tests/test_browser.py
27
27
  tests/test_frame_concurrency.py
28
28
  tests/test_process_group_kill.py
29
+ tests/test_pull_fallback.py
29
30
  tests/test_run_single_instance.py
30
31
  tests/test_session_isolation.py
@@ -0,0 +1,79 @@
1
+ """CLI 1.0 against a server that does not offer pull must use the socket.
2
+
3
+ 1.0.0 fell back only on a 404. A server from before the v2 API answers a
4
+ POST to /api/cli/v2/hello with 405 — its frontend's catch-all route — and
5
+ 1.0.0 retried that forever and never connected. Every answer that is not a
6
+ v2 hello now means "no pull here", except a rejected token (reported) and a
7
+ server error (retried).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ import httpx
17
+ import pytest
18
+
19
+ sys.path.insert(0, str(Path(__file__).resolve().parents[0] / ".." / "src"))
20
+
21
+ from virtuai_cli import pull # noqa: E402
22
+
23
+
24
+ def _hello_against(response: httpx.Response, tmp_path):
25
+ session = pull._Session("http://x", "t", tmp_path, lambda *a: None, None, "host")
26
+ session.http = httpx.AsyncClient(base_url="http://x",
27
+ transport=httpx.MockTransport(lambda req: response))
28
+ return asyncio.run(session.hello())
29
+
30
+
31
+ @pytest.mark.parametrize("response", [
32
+ httpx.Response(404, json={"detail": "Not Found"}), # v2 code, flag off
33
+ httpx.Response(405, json={"detail": "Method Not Allowed"}), # a server from before v2
34
+ httpx.Response(200, text="<!doctype html><title>VirtuAI</title>", # a catch-all page
35
+ headers={"content-type": "text/html"}),
36
+ httpx.Response(200, json={"ok": True}), # JSON, but not a hello
37
+ httpx.Response(403, json={"detail": "Forbidden"}),
38
+ ], ids=["404", "405", "html", "json-without-runner", "403"])
39
+ def test_anything_but_a_v2_hello_means_use_the_socket(response, tmp_path):
40
+ with pytest.raises(pull.PullUnavailable):
41
+ _hello_against(response, tmp_path)
42
+
43
+
44
+ def test_a_rejected_token_is_reported_not_retried(tmp_path):
45
+ with pytest.raises(pull.TokenRejected):
46
+ _hello_against(httpx.Response(401, json={"detail": "Invalid CLI token"}), tmp_path)
47
+
48
+
49
+ def test_a_server_error_is_retried_not_taken_as_no_pull(tmp_path):
50
+ with pytest.raises(httpx.HTTPStatusError):
51
+ _hello_against(httpx.Response(503, text="unavailable"), tmp_path)
52
+
53
+
54
+ def test_a_real_hello_registers(tmp_path):
55
+ _hello_against(httpx.Response(200, json={"runner_id": "c-1", "wait_seconds": 25,
56
+ "heartbeat_seconds": 5}), tmp_path)
57
+
58
+
59
+ def test_run_forever_falls_back_to_the_socket_on_an_old_server(monkeypatch, tmp_path):
60
+ """End to end in the runner: hello gets 405, the socket session starts."""
61
+ from virtuai_cli import runner
62
+
63
+ started = []
64
+
65
+ async def fake_session(*a, **kw):
66
+ started.append(True)
67
+ raise SystemExit(0)
68
+
69
+ real_client = httpx.AsyncClient
70
+
71
+ def client(*a, **kw):
72
+ kw["transport"] = httpx.MockTransport(lambda req: httpx.Response(405, json={}))
73
+ return real_client(*a, **kw)
74
+
75
+ monkeypatch.setattr(pull.httpx, "AsyncClient", client)
76
+ monkeypatch.setattr(runner, "run_session", fake_session)
77
+ monkeypatch.delenv("VIRTUAI_TRANSPORT", raising=False)
78
+ asyncio.run(runner.run_forever("https://old.example", "t", tmp_path, lambda *a: None, None))
79
+ assert started == [True]
File without changes
File without changes