virtuai-cli 1.0.3__tar.gz → 1.0.4__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 (38) hide show
  1. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/PKG-INFO +3 -3
  2. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/README.md +2 -2
  3. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/pyproject.toml +1 -1
  4. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/__init__.py +1 -1
  5. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/ollama_state.py +8 -3
  6. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/pull.py +12 -3
  7. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/runner.py +8 -6
  8. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli.egg-info/PKG-INFO +3 -3
  9. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_ollama_options.py +15 -0
  10. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/setup.cfg +0 -0
  11. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/browser.py +0 -0
  12. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/__init__.py +0 -0
  13. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/agents.py +0 -0
  14. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/ask.py +0 -0
  15. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/command.py +0 -0
  16. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/history.py +0 -0
  17. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/sse.py +0 -0
  18. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/tui.py +0 -0
  19. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/chat/widgets.py +0 -0
  20. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/config.py +0 -0
  21. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/executor.py +0 -0
  22. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/main.py +0 -0
  23. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/omlx_state.py +0 -0
  24. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/opencode.py +0 -0
  25. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli/security.py +0 -0
  26. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli.egg-info/SOURCES.txt +0 -0
  27. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli.egg-info/dependency_links.txt +0 -0
  28. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli.egg-info/entry_points.txt +0 -0
  29. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli.egg-info/requires.txt +0 -0
  30. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/src/virtuai_cli.egg-info/top_level.txt +0 -0
  31. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_browser.py +0 -0
  32. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_frame_concurrency.py +0 -0
  33. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_ollama_state.py +0 -0
  34. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_omlx.py +0 -0
  35. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_process_group_kill.py +0 -0
  36. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_pull_fallback.py +0 -0
  37. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/tests/test_run_single_instance.py +0 -0
  38. {virtuai_cli-1.0.3 → virtuai_cli-1.0.4}/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.3
3
+ Version: 1.0.4
4
4
  Summary: Run VirtuAI deep agents on your local machine
5
5
  Author-email: uCloudStore <lmoreno@ucloudstore.com>
6
6
  License: Proprietary
@@ -138,7 +138,7 @@ Each model call goes to the runner of the person the turn is for; if they have n
138
138
 
139
139
  At connect and then every minute: the models it has, the Ollama version, what is loaded and how much memory it really took. With `llmfit` installed it also estimates which pulled models fit this machine, so the server does not send a model to a runner that cannot hold it until it has actually run there. A model that spills to the CPU gets half the context on its next call on that machine (down to 4k).
140
140
 
141
- The context sent to Ollama is 8k by default (set on the server); `OLLAMA_NUM_CTX` overrides it on a machine with less memory. For qwen3 the runner turns Ollama's thinking mode off: left on, it reasoned for minutes before every tool call.
141
+ The context sent to Ollama is 16k by default (set on the server). A model that does not fit at that context — `qwen3:14b` on a 16 GB Mac spills 7% to the CPU — is capped at half on that machine after its first call, and the server is told in the same reply: from then on it sends prompts that fit, compacting the conversation when they would not, instead of letting Ollama cut the start of the prompt. `OLLAMA_NUM_CTX` sets a lower context on a machine with little memory. For qwen3 the runner turns Ollama's thinking mode off: left on, it reasoned for minutes before every tool call.
142
142
 
143
143
  ### oMLX
144
144
 
@@ -163,7 +163,7 @@ mean "allow more commands at once" silently launched more browsers.
163
163
  | `VIRTUAI_SANDBOX_OWNER` | unset | The one person this runner serves, reported to the server so a browser action reaches *their* browser. Leave it unset on a laptop or a container shared by a team: the server then routes to it for anyone, which is the behaviour that existed before per-person sandboxes. Set it when a workspace has a sandbox per person, or actions land on whichever runner connected last. |
164
164
  | `VIRTUAI_SERVE_MODELS` | `own` | Whose local-model turns this runner answers: `own`, `workspace` or `none`. Also set by `--serve-models`. |
165
165
  | `OLLAMA_BASE_URL` | `http://localhost:11434` | Where to reach the local Ollama. |
166
- | `OLLAMA_NUM_CTX` | unset | The context to ask Ollama for, instead of the server's (8k). Lower it on a machine with little memory. |
166
+ | `OLLAMA_NUM_CTX` | unset | The context to ask Ollama for, instead of the server's (16k). Lower it on a machine with little memory. |
167
167
  | `VIRTUAI_OMLX_URL` | from `~/.omlx/settings.json`, else `http://127.0.0.1:8000` | Where to reach oMLX. |
168
168
  | `OMLX_API_KEY` | from `~/.omlx/settings.json` | oMLX's API key. Stays on this machine. |
169
169
 
@@ -109,7 +109,7 @@ Each model call goes to the runner of the person the turn is for; if they have n
109
109
 
110
110
  At connect and then every minute: the models it has, the Ollama version, what is loaded and how much memory it really took. With `llmfit` installed it also estimates which pulled models fit this machine, so the server does not send a model to a runner that cannot hold it until it has actually run there. A model that spills to the CPU gets half the context on its next call on that machine (down to 4k).
111
111
 
112
- The context sent to Ollama is 8k by default (set on the server); `OLLAMA_NUM_CTX` overrides it on a machine with less memory. For qwen3 the runner turns Ollama's thinking mode off: left on, it reasoned for minutes before every tool call.
112
+ The context sent to Ollama is 16k by default (set on the server). A model that does not fit at that context — `qwen3:14b` on a 16 GB Mac spills 7% to the CPU — is capped at half on that machine after its first call, and the server is told in the same reply: from then on it sends prompts that fit, compacting the conversation when they would not, instead of letting Ollama cut the start of the prompt. `OLLAMA_NUM_CTX` sets a lower context on a machine with little memory. For qwen3 the runner turns Ollama's thinking mode off: left on, it reasoned for minutes before every tool call.
113
113
 
114
114
  ### oMLX
115
115
 
@@ -134,7 +134,7 @@ mean "allow more commands at once" silently launched more browsers.
134
134
  | `VIRTUAI_SANDBOX_OWNER` | unset | The one person this runner serves, reported to the server so a browser action reaches *their* browser. Leave it unset on a laptop or a container shared by a team: the server then routes to it for anyone, which is the behaviour that existed before per-person sandboxes. Set it when a workspace has a sandbox per person, or actions land on whichever runner connected last. |
135
135
  | `VIRTUAI_SERVE_MODELS` | `own` | Whose local-model turns this runner answers: `own`, `workspace` or `none`. Also set by `--serve-models`. |
136
136
  | `OLLAMA_BASE_URL` | `http://localhost:11434` | Where to reach the local Ollama. |
137
- | `OLLAMA_NUM_CTX` | unset | The context to ask Ollama for, instead of the server's (8k). Lower it on a machine with little memory. |
137
+ | `OLLAMA_NUM_CTX` | unset | The context to ask Ollama for, instead of the server's (16k). Lower it on a machine with little memory. |
138
138
  | `VIRTUAI_OMLX_URL` | from `~/.omlx/settings.json`, else `http://127.0.0.1:8000` | Where to reach oMLX. |
139
139
  | `OMLX_API_KEY` | from `~/.omlx/settings.json` | oMLX's API key. Stays on this machine. |
140
140
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "virtuai-cli"
7
- version = "1.0.3"
7
+ version = "1.0.4"
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.3"
2
+ __version__ = "1.0.4"
@@ -120,7 +120,7 @@ async def after_call(model: str) -> None:
120
120
  info = (state.get("loaded") or {}).get(model)
121
121
  if not info or info.get("on_gpu_pct", 100) >= 100:
122
122
  return
123
- current = info.get("num_ctx") or _ctx_cap.get(model) or 8_192
123
+ current = info.get("num_ctx") or _ctx_cap.get(model) or SERVER_NUM_CTX
124
124
  lower = max(MIN_NUM_CTX, current // 2)
125
125
  if lower < current:
126
126
  _ctx_cap[model] = lower
@@ -215,11 +215,16 @@ async def current(num_ctx: int) -> dict:
215
215
  return state
216
216
 
217
217
 
218
+ #: The context the server asks for unless told otherwise (its
219
+ #: VIRTUAI_LOCAL_NUM_CTX); what the fit report estimates at.
220
+ SERVER_NUM_CTX = 16_384
221
+
222
+
218
223
  def _num_ctx() -> int:
219
224
  try:
220
- return int(os.environ.get("OLLAMA_NUM_CTX") or 8_192)
225
+ return int(os.environ.get("OLLAMA_NUM_CTX") or SERVER_NUM_CTX)
221
226
  except ValueError:
222
- return 8_192
227
+ return SERVER_NUM_CTX
223
228
 
224
229
 
225
230
  async def snapshot() -> dict:
@@ -303,9 +303,18 @@ class _Session:
303
303
  continue
304
304
  if sock.done.is_set():
305
305
  break
306
+ # The beat's clock starts when it is sent, and a slow one gives up
307
+ # at two intervals: it used to wait out the client's 40 s read
308
+ # timeout, and the beats behind it waited too — on a laptop busy
309
+ # with a local model and a `find /`, long enough for the server
310
+ # to declare the job lost while it ran.
311
+ last_beat = time.monotonic()
306
312
  try:
307
- reply = await self.http.post(f"{base}/heartbeat", json={"events": sock.take_events()})
308
- last_beat = time.monotonic()
313
+ # wait_for bounds the whole request; httpx's timeout is per
314
+ # read, and a reply trickling in would get past it.
315
+ reply = await asyncio.wait_for(
316
+ self.http.post(f"{base}/heartbeat", json={"events": sock.take_events()}),
317
+ timeout=max(2.0, 2 * self.heartbeat))
309
318
  if reply.status_code == 200 and reply.json().get("cancel"):
310
319
  self.on_status("warn", f"Job {job_id} cancelled by the server")
311
320
  if task is not None:
@@ -314,7 +323,7 @@ class _Session:
314
323
  "v": 1, "id": job.get("id"), "type": "error", "error": "cancelled by the server"}
315
324
  sock.done.set()
316
325
  except Exception as exc:
317
- logger.debug("Heartbeat for %s failed: %s", job_id, exc)
326
+ logger.warning("Heartbeat for %s failed: %s", job_id, exc)
318
327
 
319
328
  final = sock.final or runner._busy_reply(job, "the runner finished this request without a reply") or {
320
329
  "v": 1, "id": job.get("id"), "type": "error", "error": "no reply"}
@@ -222,18 +222,20 @@ async def _handle_model_generate(frame: dict, ws, on_status: StatusCallback) ->
222
222
 
223
223
  tool_calls = list(getattr(gathered, "tool_calls", []) or []) if gathered else []
224
224
  usage = dict(getattr(gathered, "usage_metadata", None) or {}) if gathered else {}
225
- await ws.send(json.dumps({
226
- "v": 1, "id": fid, "type": "model_result",
227
- "tool_calls": tool_calls, "usage": usage,
228
- }))
229
- # After the reply, so the user never waits on it: what the model
230
- # really took decides the context of its next call here.
225
+ result = {"v": 1, "id": fid, "type": "model_result", "tool_calls": tool_calls, "usage": usage}
231
226
  if not omlx:
227
+ # What the model really took decides the context of its next call
228
+ # here, and the server hears it in this reply — not a minute later
229
+ # in a status report, by which time it may have sent a prompt this
230
+ # context would cut. A local /api/ps: milliseconds.
232
231
  from virtuai_cli import ollama_state
233
232
  try:
234
233
  await asyncio.wait_for(ollama_state.after_call(model), timeout=5.0)
235
234
  except Exception:
236
235
  pass
236
+ if ollama_state.context_for(model, None):
237
+ result["context_cap"] = ollama_state.context_for(model, None)
238
+ await ws.send(json.dumps(result))
237
239
  except Exception as exc:
238
240
  logger.warning("model_generate failed: %s", exc)
239
241
  await _error(str(exc))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: virtuai-cli
3
- Version: 1.0.3
3
+ Version: 1.0.4
4
4
  Summary: Run VirtuAI deep agents on your local machine
5
5
  Author-email: uCloudStore <lmoreno@ucloudstore.com>
6
6
  License: Proprietary
@@ -138,7 +138,7 @@ Each model call goes to the runner of the person the turn is for; if they have n
138
138
 
139
139
  At connect and then every minute: the models it has, the Ollama version, what is loaded and how much memory it really took. With `llmfit` installed it also estimates which pulled models fit this machine, so the server does not send a model to a runner that cannot hold it until it has actually run there. A model that spills to the CPU gets half the context on its next call on that machine (down to 4k).
140
140
 
141
- The context sent to Ollama is 8k by default (set on the server); `OLLAMA_NUM_CTX` overrides it on a machine with less memory. For qwen3 the runner turns Ollama's thinking mode off: left on, it reasoned for minutes before every tool call.
141
+ The context sent to Ollama is 16k by default (set on the server). A model that does not fit at that context — `qwen3:14b` on a 16 GB Mac spills 7% to the CPU — is capped at half on that machine after its first call, and the server is told in the same reply: from then on it sends prompts that fit, compacting the conversation when they would not, instead of letting Ollama cut the start of the prompt. `OLLAMA_NUM_CTX` sets a lower context on a machine with little memory. For qwen3 the runner turns Ollama's thinking mode off: left on, it reasoned for minutes before every tool call.
142
142
 
143
143
  ### oMLX
144
144
 
@@ -163,7 +163,7 @@ mean "allow more commands at once" silently launched more browsers.
163
163
  | `VIRTUAI_SANDBOX_OWNER` | unset | The one person this runner serves, reported to the server so a browser action reaches *their* browser. Leave it unset on a laptop or a container shared by a team: the server then routes to it for anyone, which is the behaviour that existed before per-person sandboxes. Set it when a workspace has a sandbox per person, or actions land on whichever runner connected last. |
164
164
  | `VIRTUAI_SERVE_MODELS` | `own` | Whose local-model turns this runner answers: `own`, `workspace` or `none`. Also set by `--serve-models`. |
165
165
  | `OLLAMA_BASE_URL` | `http://localhost:11434` | Where to reach the local Ollama. |
166
- | `OLLAMA_NUM_CTX` | unset | The context to ask Ollama for, instead of the server's (8k). Lower it on a machine with little memory. |
166
+ | `OLLAMA_NUM_CTX` | unset | The context to ask Ollama for, instead of the server's (16k). Lower it on a machine with little memory. |
167
167
  | `VIRTUAI_OMLX_URL` | from `~/.omlx/settings.json`, else `http://127.0.0.1:8000` | Where to reach oMLX. |
168
168
  | `OMLX_API_KEY` | from `~/.omlx/settings.json` | oMLX's API key. Stays on this machine. |
169
169
 
@@ -134,3 +134,18 @@ def test_the_runner_reports_whose_turns_it_serves(monkeypatch, env, expected):
134
134
  payload = asyncio.run(R._ready_payload(Path("/tmp")))
135
135
  assert payload["models"] == {"serve": expected}
136
136
 
137
+
138
+
139
+ def test_a_model_that_spilled_reports_its_cap_in_the_same_reply(monkeypatch):
140
+ """The server hears it on this call, not a minute later in a status report."""
141
+ from virtuai_cli import ollama_state
142
+
143
+ async def spilled(model):
144
+ ollama_state._ctx_cap[model] = 8192
145
+
146
+ monkeypatch.setattr(ollama_state, "after_call", spilled)
147
+ monkeypatch.setattr(ollama_state, "_ctx_cap", {})
148
+ built, sent = _run({**FRAME, "num_ctx": 16384}, monkeypatch)
149
+ assert built["num_ctx"] == 16384 and sent[-1]["context_cap"] == 8192
150
+ built, sent = _run({**FRAME, "num_ctx": 16384}, monkeypatch)
151
+ assert built["num_ctx"] == 8192, "and asks Ollama for less from then on"
File without changes