spawnllm 0.17.3__tar.gz → 0.17.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 (35) hide show
  1. {spawnllm-0.17.3 → spawnllm-0.17.4}/PKG-INFO +1 -1
  2. {spawnllm-0.17.3 → spawnllm-0.17.4}/pyproject.toml +1 -1
  3. {spawnllm-0.17.3 → spawnllm-0.17.4}/pyproject.toml.orig +1 -1
  4. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/backends/base.py +41 -2
  5. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/backends/claude.py +16 -8
  6. {spawnllm-0.17.3 → spawnllm-0.17.4}/LICENSE +0 -0
  7. {spawnllm-0.17.3 → spawnllm-0.17.4}/README.md +0 -0
  8. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/__init__.py +0 -0
  9. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/__main__.py +0 -0
  10. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/_core/__init__.py +0 -0
  11. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/_core/spawnllm_core.wasm +0 -0
  12. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/backends/__init__.py +0 -0
  13. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/backends/apple.py +0 -0
  14. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/backends/claude_sdk.py +0 -0
  15. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/backends/codex.py +0 -0
  16. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/backends/gemini.py +0 -0
  17. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/backends/mlx.py +0 -0
  18. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/backends/openai_endpoint.py +0 -0
  19. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/backends/registry.py +0 -0
  20. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/call.py +0 -0
  21. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/cli.py +0 -0
  22. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/decide.py +0 -0
  23. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/extract.py +0 -0
  24. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/mlx/__init__.py +0 -0
  25. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/mlx/codec.py +0 -0
  26. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/mlx/engine.py +0 -0
  27. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/mlx/fuse.py +0 -0
  28. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/mlx/patches.py +0 -0
  29. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/proc.py +0 -0
  30. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/py.typed +0 -0
  31. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/response.py +0 -0
  32. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/run.py +0 -0
  33. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/spec.py +0 -0
  34. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/structured.py +0 -0
  35. {spawnllm-0.17.3 → spawnllm-0.17.4}/spawnllm/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawnllm
3
- Version: 0.17.3
3
+ Version: 0.17.4
4
4
  Summary: Delete your subprocess wrappers around claude, codex, and gemini.
5
5
  Keywords:
6
6
  Author: Yasyf Mohamedali
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spawnllm"
3
- version = "0.17.3"
3
+ version = "0.17.4"
4
4
  description = "Delete your subprocess wrappers around claude, codex, and gemini."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "spawnllm"
3
3
  # Inert sentinel: the real version is set from the release tag (uv version --frozen).
4
- version = "0.17.3"
4
+ version = "0.17.4"
5
5
  description = "Delete your subprocess wrappers around claude, codex, and gemini."
6
6
  readme = "README.md"
7
7
  license = "MIT"
@@ -17,6 +17,10 @@ import subprocess
17
17
  import sys
18
18
  import tempfile
19
19
  from abc import ABC, abstractmethod
20
+ from collections.abc import Iterator
21
+ from contextlib import contextmanager
22
+ from contextvars import ContextVar
23
+ from copy import deepcopy
20
24
  from dataclasses import dataclass
21
25
  from pathlib import Path
22
26
  from typing import TYPE_CHECKING, Any, ClassVar
@@ -33,6 +37,9 @@ if TYPE_CHECKING:
33
37
  from spawnllm.types import ProviderName, TModel
34
38
 
35
39
 
40
+ INVOCATION_PLAN: ContextVar[InvocationPlanState | None] = ContextVar("spawnllm_invocation_plan", default=None)
41
+
42
+
36
43
  @dataclass(frozen=True)
37
44
  class BackendReady:
38
45
  """A backend whose CLI is installed and authenticated.
@@ -164,6 +171,14 @@ def run_probe(probe: dict[str, Any], *, binary: str, timeout: int) -> bool:
164
171
  return False
165
172
 
166
173
 
174
+ @dataclass(slots=True)
175
+ class InvocationPlanState:
176
+ backend: LlmBackend
177
+ spec: RunSpec
178
+ value: dict[str, Any] | None = None
179
+ active: bool = True
180
+
181
+
167
182
  class LlmBackend(ABC):
168
183
  """Abstract execution contract for an LLM backend.
169
184
 
@@ -312,6 +327,14 @@ class LlmBackend(ABC):
312
327
 
313
328
  def core_plan(self, spec: RunSpec) -> dict[str, Any]:
314
329
  """Ask the core to plan this run's invocation (an exec argv or an HTTP request)."""
330
+ pending = INVOCATION_PLAN.get()
331
+ if pending is not None and pending.active and pending.backend is self and pending.spec is spec:
332
+ if pending.value is None:
333
+ pending.value = self._core_plan(spec)
334
+ return deepcopy(pending.value)
335
+ return self._core_plan(spec)
336
+
337
+ def _core_plan(self, spec: RunSpec) -> dict[str, Any]:
315
338
  return _core.dispatch(
316
339
  "plan", {"host": {"platform": sys.platform}, "provider": self.provider, "spec": self.wire_spec(spec)}
317
340
  )
@@ -387,6 +410,16 @@ class CliBackend(LlmBackend):
387
410
  binary: ClassVar[str]
388
411
  install_hint: ClassVar[str]
389
412
 
413
+ @contextmanager
414
+ def _reuse_plan(self, spec: RunSpec) -> Iterator[None]:
415
+ pending = InvocationPlanState(self, spec)
416
+ token = INVOCATION_PLAN.set(pending)
417
+ try:
418
+ yield
419
+ finally:
420
+ pending.active = False
421
+ INVOCATION_PLAN.reset(token)
422
+
390
423
  def binary_path(self) -> str:
391
424
  """Return the command every exec of this backend runs as `argv[0]`.
392
425
 
@@ -500,7 +533,10 @@ class CliBackend(LlmBackend):
500
533
  return Response(spec=spec, output=Output(""), error=Error(msg, TimeoutError(msg)))
501
534
 
502
535
  async def aexecute(self, spec: RunSpec) -> Response:
503
- return await self.aexecute_invocation(spec, self.invocation(spec), self.env(spec))
536
+ with self._reuse_plan(spec):
537
+ inv = self.invocation(spec)
538
+ env = self.env(spec)
539
+ return await self.aexecute_invocation(spec, inv, env)
504
540
 
505
541
  async def aexecute_invocation(self, spec: RunSpec, inv: Invocation, env: dict[str, str]) -> Response:
506
542
  """Run a materialized `inv` asynchronously under `env` and resolve it, removing its temp files."""
@@ -525,7 +561,10 @@ class CliBackend(LlmBackend):
525
561
  return self.to_response(raw, returncode=rr.returncode, stderr=rr.stderr, spec=spec)
526
562
 
527
563
  def execute(self, spec: RunSpec) -> Response:
528
- return self.execute_invocation(spec, self.invocation(spec), self.env(spec))
564
+ with self._reuse_plan(spec):
565
+ inv = self.invocation(spec)
566
+ env = self.env(spec)
567
+ return self.execute_invocation(spec, inv, env)
529
568
 
530
569
  def execute_invocation(self, spec: RunSpec, inv: Invocation, env: dict[str, str]) -> Response:
531
570
  """Run a materialized `inv` under `env` and resolve it, removing its temp files."""
@@ -146,17 +146,25 @@ class ClaudeCliBackend(CliBackend):
146
146
  return self.claude_isolation(spec.api_auth).env.get("CLAUDE_CODE_OAUTH_TOKEN") not in self._rejected_tokens
147
147
 
148
148
  async def aexecute(self, spec: RunSpec) -> Response:
149
- env = self.env(spec)
150
- response = await self.aexecute_invocation(spec, self.invocation(spec), env)
151
- while self.reject_credentials(spec, env, response):
149
+ with self._reuse_plan(spec):
152
150
  env = self.env(spec)
153
- response = await self.aexecute_invocation(spec, self.invocation(spec), env)
151
+ inv = self.invocation(spec)
152
+ response = await self.aexecute_invocation(spec, inv, env)
153
+ while self.reject_credentials(spec, env, response):
154
+ with self._reuse_plan(spec):
155
+ env = self.env(spec)
156
+ inv = self.invocation(spec)
157
+ response = await self.aexecute_invocation(spec, inv, env)
154
158
  return response
155
159
 
156
160
  def execute(self, spec: RunSpec) -> Response:
157
- env = self.env(spec)
158
- response = self.execute_invocation(spec, self.invocation(spec), env)
159
- while self.reject_credentials(spec, env, response):
161
+ with self._reuse_plan(spec):
160
162
  env = self.env(spec)
161
- response = self.execute_invocation(spec, self.invocation(spec), env)
163
+ inv = self.invocation(spec)
164
+ response = self.execute_invocation(spec, inv, env)
165
+ while self.reject_credentials(spec, env, response):
166
+ with self._reuse_plan(spec):
167
+ env = self.env(spec)
168
+ inv = self.invocation(spec)
169
+ response = self.execute_invocation(spec, inv, env)
162
170
  return response
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