spawnllm 0.1.3__tar.gz → 0.3.0__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 (32) hide show
  1. {spawnllm-0.1.3 → spawnllm-0.3.0}/PKG-INFO +75 -14
  2. spawnllm-0.3.0/README.md +123 -0
  3. {spawnllm-0.1.3 → spawnllm-0.3.0}/pyproject.toml +6 -1
  4. {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/__init__.py +20 -14
  5. spawnllm-0.3.0/spawnllm/backends/__init__.py +33 -0
  6. spawnllm-0.3.0/spawnllm/backends/base.py +184 -0
  7. spawnllm-0.3.0/spawnllm/backends/claude.py +194 -0
  8. spawnllm-0.3.0/spawnllm/backends/codex.py +137 -0
  9. spawnllm-0.3.0/spawnllm/backends/gemini.py +226 -0
  10. spawnllm-0.3.0/spawnllm/backends/registry.py +80 -0
  11. spawnllm-0.3.0/spawnllm/call.py +54 -0
  12. spawnllm-0.3.0/spawnllm/cli.py +66 -0
  13. {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/mlx/__init__.py +2 -2
  14. {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/mlx/codec.py +26 -6
  15. {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/mlx/engine.py +49 -5
  16. {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/mlx/fuse.py +23 -0
  17. {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/mlx/patches.py +11 -1
  18. {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/proc.py +53 -1
  19. spawnllm-0.3.0/spawnllm/structured.py +120 -0
  20. {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/types.py +3 -0
  21. spawnllm-0.1.3/README.md +0 -64
  22. spawnllm-0.1.3/spawnllm/backends/__init__.py +0 -27
  23. spawnllm-0.1.3/spawnllm/backends/base.py +0 -53
  24. spawnllm-0.1.3/spawnllm/backends/claude.py +0 -126
  25. spawnllm-0.1.3/spawnllm/backends/codex.py +0 -41
  26. spawnllm-0.1.3/spawnllm/backends/registry.py +0 -27
  27. spawnllm-0.1.3/spawnllm/call.py +0 -42
  28. spawnllm-0.1.3/spawnllm/cli.py +0 -40
  29. spawnllm-0.1.3/spawnllm/structured.py +0 -68
  30. {spawnllm-0.1.3 → spawnllm-0.3.0}/LICENSE +0 -0
  31. {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/__main__.py +0 -0
  32. {spawnllm-0.1.3 → spawnllm-0.3.0}/spawnllm/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawnllm
3
- Version: 0.1.3
3
+ Version: 0.3.0
4
4
  Summary: Subshell + MLX LLM-calling backends (Claude/Codex CLI, local MLX) shared across tools.
5
5
  Keywords:
6
6
  Author: Yasyf Mohamedali
@@ -16,6 +16,8 @@ Classifier: Typing :: Typed
16
16
  Requires-Dist: click>=8
17
17
  Requires-Dist: loguru>=0.7
18
18
  Requires-Dist: pydantic>=2
19
+ Requires-Dist: openai>=2.43
20
+ Requires-Dist: anthropic>=0.111
19
21
  Requires-Dist: zstandard>=0.25.0 ; extra == 'adapter'
20
22
  Requires-Dist: numpy>=1.26 ; extra == 'adapter'
21
23
  Requires-Dist: orjson>=3.10 ; extra == 'adapter'
@@ -45,6 +47,8 @@ Description-Content-Type: text/markdown
45
47
 
46
48
  # spawnllm
47
49
 
50
+ ![spawnllm banner](https://github.com/yasyf/spawnllm/raw/main/docs/assets/readme-banner.webp)
51
+
48
52
  [![PyPI](https://img.shields.io/pypi/v/spawnllm.svg)](https://pypi.org/project/spawnllm/)
49
53
  [![Python](https://img.shields.io/pypi/pyversions/spawnllm.svg)](https://pypi.org/project/spawnllm/)
50
54
  [![Docs](https://img.shields.io/github/actions/workflow/status/yasyf/spawnllm/docs.yml?branch=main&label=docs)](https://yasyf.github.io/spawnllm/)
@@ -81,28 +85,85 @@ uv add "spawnllm[mlx]"
81
85
 
82
86
  ## Quickstart
83
87
 
84
- List the backends spawnllm can drive:
88
+ See which backends are installed and authenticated, and which one auto-selection picks:
85
89
 
86
90
  ```bash
87
- uvx spawnllm backends
91
+ uvx spawnllm status
92
+ ```
93
+
94
+ ```
95
+ claude: ready
96
+ codex: ready
97
+ selected: claude
88
98
  ```
89
99
 
100
+ Make a request by passing a prompt as the argument, or piping it over stdin:
101
+
102
+ ```bash
103
+ uvx spawnllm call --backend claude "What is 2+2? Reply with just the number."
104
+ ```
105
+
106
+ ```
107
+ 4
108
+ ```
109
+
110
+ `--model small|medium|large` swaps the tier, which each backend maps to a concrete model.
111
+ The `claude` backend resolves `small` to Haiku, `medium` to Sonnet, and `large` to Opus. Add
112
+ `--agent` to let the call use tools.
113
+
114
+ ### From Python
115
+
116
+ `call` runs one request and returns the response. With no `backend`, it auto-selects the
117
+ first installed, authenticated CLI:
118
+
119
+ ```python
120
+ from spawnllm import call
121
+
122
+ print(call("Reply with just the word: pong"))
123
+ # pong
90
124
  ```
91
- claude
92
- codex
93
- mlx
125
+
126
+ Pin a backend and tier explicitly, or pass a Pydantic model to get a validated object back
127
+ instead of text:
128
+
129
+ ```python
130
+ from pydantic import BaseModel
131
+
132
+ from spawnllm import call, ClaudeCliBackend
133
+
134
+
135
+ class Capital(BaseModel):
136
+ country: str
137
+ capital: str
138
+
139
+
140
+ result = call(
141
+ "What is the capital of France?",
142
+ backend=ClaudeCliBackend(),
143
+ model="large",
144
+ response_model=Capital,
145
+ )
146
+ print(result.capital) # Paris
94
147
  ```
95
148
 
149
+ When you don't pin a backend, set `specialty=` to scope auto-selection by task. The
150
+ `debugging` and `review` specialties route to Codex, and `general` routes to Claude.
151
+
96
152
  ## What problems does this solve?
97
153
 
98
- - **Duplicate subshell plumbing.** Building `claude`/`codex` argv, piping stdin/stdout, teeing
99
- stderr, and turning non-zero exits into useful errors — written once, not re-derived per tool.
100
- - **Structured-output boilerplate.** A Pydantic model becomes a JSON-schema constraint and a
101
- parsed, validated result the same way for every backend.
102
- - **Local MLX is fiddly.** Adapter fusion, prompt-cache reuse, worker-thread lifecycle, and
103
- batched single-token generation live behind one engine instead of in every consumer.
104
- - **Behavior drift.** Two tools that call the same models stay byte-for-byte consistent because
105
- they share the backend layer rather than each maintaining a copy.
154
+ Every tool that shells out to `claude` or `codex` rebuilds the same plumbing: argv
155
+ construction, stdin/stdout piping, stderr teeing, and turning non-zero exits into useful
156
+ errors. spawnllm holds it once.
157
+
158
+ Structured output is boilerplate too. A Pydantic model becomes a JSON-schema constraint
159
+ and a parsed, validated result, identically for both CLI backends.
160
+
161
+ Local MLX is fiddly. Adapter fusion, prompt-cache reuse, worker-thread lifecycle, and
162
+ batched single-token generation live behind one engine instead of in every consumer.
163
+
164
+ Behavior drift goes away with the duplication: two tools that call the same models stay
165
+ byte-for-byte consistent because they share the backend layer, not a pair of diverging
166
+ copies.
106
167
 
107
168
  ## Docs
108
169
 
@@ -0,0 +1,123 @@
1
+ # spawnllm
2
+
3
+ ![spawnllm banner](https://github.com/yasyf/spawnllm/raw/main/docs/assets/readme-banner.webp)
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/spawnllm.svg)](https://pypi.org/project/spawnllm/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/spawnllm.svg)](https://pypi.org/project/spawnllm/)
7
+ [![Docs](https://img.shields.io/github/actions/workflow/status/yasyf/spawnllm/docs.yml?branch=main&label=docs)](https://yasyf.github.io/spawnllm/)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/yasyf/spawnllm/blob/main/LICENSE)
9
+
10
+ Subshell + MLX LLM-calling backends (Claude/Codex CLI, local MLX) shared across tools.
11
+
12
+ spawnllm centralizes the LLM-calling plumbing that small tools keep re-inventing: driving the
13
+ `claude` and `codex` CLIs as subshells — with structured Pydantic output, model tiers, and
14
+ faithful error capture — and running local Apple-Silicon MLX models with adapter fusion,
15
+ prompt-cache reuse, and batched generation. Depend on it once and each tool keeps only its
16
+ domain logic instead of its own copy of the backends.
17
+
18
+ ## Install
19
+
20
+ No install needed — run everything through [uvx](https://docs.astral.sh/uv/):
21
+
22
+ ```bash
23
+ uvx spawnllm --help
24
+ ```
25
+
26
+ `uvx` fetches spawnllm into a throwaway environment and runs it. To add it
27
+ to a project instead:
28
+
29
+ ```bash
30
+ uv add spawnllm
31
+ ```
32
+
33
+ For the local MLX engine (Apple Silicon only), pull the extra:
34
+
35
+ ```bash
36
+ uv add "spawnllm[mlx]"
37
+ ```
38
+
39
+ ## Quickstart
40
+
41
+ See which backends are installed and authenticated, and which one auto-selection picks:
42
+
43
+ ```bash
44
+ uvx spawnllm status
45
+ ```
46
+
47
+ ```
48
+ claude: ready
49
+ codex: ready
50
+ selected: claude
51
+ ```
52
+
53
+ Make a request by passing a prompt as the argument, or piping it over stdin:
54
+
55
+ ```bash
56
+ uvx spawnllm call --backend claude "What is 2+2? Reply with just the number."
57
+ ```
58
+
59
+ ```
60
+ 4
61
+ ```
62
+
63
+ `--model small|medium|large` swaps the tier, which each backend maps to a concrete model.
64
+ The `claude` backend resolves `small` to Haiku, `medium` to Sonnet, and `large` to Opus. Add
65
+ `--agent` to let the call use tools.
66
+
67
+ ### From Python
68
+
69
+ `call` runs one request and returns the response. With no `backend`, it auto-selects the
70
+ first installed, authenticated CLI:
71
+
72
+ ```python
73
+ from spawnllm import call
74
+
75
+ print(call("Reply with just the word: pong"))
76
+ # pong
77
+ ```
78
+
79
+ Pin a backend and tier explicitly, or pass a Pydantic model to get a validated object back
80
+ instead of text:
81
+
82
+ ```python
83
+ from pydantic import BaseModel
84
+
85
+ from spawnllm import call, ClaudeCliBackend
86
+
87
+
88
+ class Capital(BaseModel):
89
+ country: str
90
+ capital: str
91
+
92
+
93
+ result = call(
94
+ "What is the capital of France?",
95
+ backend=ClaudeCliBackend(),
96
+ model="large",
97
+ response_model=Capital,
98
+ )
99
+ print(result.capital) # Paris
100
+ ```
101
+
102
+ When you don't pin a backend, set `specialty=` to scope auto-selection by task. The
103
+ `debugging` and `review` specialties route to Codex, and `general` routes to Claude.
104
+
105
+ ## What problems does this solve?
106
+
107
+ Every tool that shells out to `claude` or `codex` rebuilds the same plumbing: argv
108
+ construction, stdin/stdout piping, stderr teeing, and turning non-zero exits into useful
109
+ errors. spawnllm holds it once.
110
+
111
+ Structured output is boilerplate too. A Pydantic model becomes a JSON-schema constraint
112
+ and a parsed, validated result, identically for both CLI backends.
113
+
114
+ Local MLX is fiddly. Adapter fusion, prompt-cache reuse, worker-thread lifecycle, and
115
+ batched single-token generation live behind one engine instead of in every consumer.
116
+
117
+ Behavior drift goes away with the duplication: two tools that call the same models stay
118
+ byte-for-byte consistent because they share the backend layer, not a pair of diverging
119
+ copies.
120
+
121
+ ## Docs
122
+
123
+ [Read the docs](https://yasyf.github.io/spawnllm/) for the full guide and API reference.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spawnllm"
3
- version = "0.1.3"
3
+ version = "0.3.0"
4
4
  description = "Subshell + MLX LLM-calling backends (Claude/Codex CLI, local MLX) shared across tools."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -20,6 +20,11 @@ dependencies = [
20
20
  "click>=8",
21
21
  "loguru>=0.7",
22
22
  "pydantic>=2",
23
+ # Per-backend strict-schema transforms: openai.lib._pydantic.to_strict_json_schema
24
+ # and anthropic.lib._parse._transform.transform_schema. Both import paths are
25
+ # SDK-private, so the floors pin verified-working versions.
26
+ "openai>=2.43",
27
+ "anthropic>=0.111",
23
28
  ]
24
29
 
25
30
  [project.optional-dependencies]
@@ -1,22 +1,26 @@
1
1
  """Subshell + MLX LLM-calling backends (Claude/Codex CLI, local MLX) shared across tools.
2
2
 
3
3
  The top-level namespace exposes the CLI backends, subprocess transport, and
4
- structured-output helpers. The MLX engine lives under :mod:`spawnllm.mlx` and is
5
- imported lazily so that ``import spawnllm`` never pulls ``mlx_lm``/``zstandard``.
4
+ structured-output helpers. The MLX engine lives under `spawnllm.mlx`, whose
5
+ imports are lazy so that `import spawnllm` never pulls `mlx_lm`/`zstandard`.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
10
  from spawnllm.backends import (
11
+ AntigravityCliBackend,
12
+ BackendNotAuthenticated,
13
+ BackendNotInstalled,
14
+ BackendReady,
15
+ BackendStatus,
16
+ BackendUnavailable,
11
17
  ClaudeCliBackend,
12
- ClaudeNotAuthenticated,
13
- ClaudeNotInstalled,
14
- ClaudeReady,
15
- ClaudeStatus,
16
18
  CodexCliBackend,
19
+ GeminiCliBackend,
20
+ Invocation,
17
21
  LlmBackend,
18
22
  LlmBackends,
19
- check_status,
23
+ select_backend,
20
24
  )
21
25
  from spawnllm.call import call
22
26
  from spawnllm.proc import arun_cli, collect_process, map_concurrent, run_cli
@@ -25,24 +29,26 @@ from spawnllm.structured import (
25
29
  parse_result_envelope,
26
30
  parse_structured_output,
27
31
  resolve_schema_path,
28
- schema_for,
29
32
  )
30
33
  from spawnllm.types import TModel, TSpecialty
31
34
 
32
35
  __all__ = [
36
+ "AntigravityCliBackend",
37
+ "BackendNotAuthenticated",
38
+ "BackendNotInstalled",
39
+ "BackendReady",
40
+ "BackendStatus",
41
+ "BackendUnavailable",
33
42
  "ClaudeCliBackend",
34
- "ClaudeNotAuthenticated",
35
- "ClaudeNotInstalled",
36
- "ClaudeReady",
37
- "ClaudeStatus",
38
43
  "CodexCliBackend",
44
+ "GeminiCliBackend",
45
+ "Invocation",
39
46
  "LlmBackend",
40
47
  "LlmBackends",
41
48
  "TModel",
42
49
  "TSpecialty",
43
50
  "arun_cli",
44
51
  "call",
45
- "check_status",
46
52
  "collect_process",
47
53
  "extract_structured",
48
54
  "map_concurrent",
@@ -50,5 +56,5 @@ __all__ = [
50
56
  "parse_structured_output",
51
57
  "resolve_schema_path",
52
58
  "run_cli",
53
- "schema_for",
59
+ "select_backend",
54
60
  ]
@@ -0,0 +1,33 @@
1
+ """LLM CLI backends (Claude/Codex/Gemini family) and the specialty registry."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from spawnllm.backends.base import (
6
+ BackendNotAuthenticated,
7
+ BackendNotInstalled,
8
+ BackendReady,
9
+ BackendStatus,
10
+ BackendUnavailable,
11
+ Invocation,
12
+ LlmBackend,
13
+ )
14
+ from spawnllm.backends.claude import ClaudeCliBackend
15
+ from spawnllm.backends.codex import CodexCliBackend
16
+ from spawnllm.backends.gemini import AntigravityCliBackend, GeminiCliBackend
17
+ from spawnllm.backends.registry import LlmBackends, select_backend
18
+
19
+ __all__ = [
20
+ "AntigravityCliBackend",
21
+ "BackendNotAuthenticated",
22
+ "BackendNotInstalled",
23
+ "BackendReady",
24
+ "BackendStatus",
25
+ "BackendUnavailable",
26
+ "ClaudeCliBackend",
27
+ "CodexCliBackend",
28
+ "GeminiCliBackend",
29
+ "Invocation",
30
+ "LlmBackend",
31
+ "LlmBackends",
32
+ "select_backend",
33
+ ]
@@ -0,0 +1,184 @@
1
+ """Abstract interface for an LLM CLI backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import shutil
7
+ from abc import ABC, abstractmethod
8
+ from dataclasses import dataclass
9
+ from typing import TYPE_CHECKING, ClassVar
10
+
11
+ if TYPE_CHECKING:
12
+ from pydantic import BaseModel
13
+
14
+ from spawnllm.types import TModel
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class BackendReady:
19
+ """A backend whose CLI is installed and authenticated.
20
+
21
+ Attributes:
22
+ binary: Name of the backend's CLI executable on PATH.
23
+ """
24
+
25
+ binary: str
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class BackendNotInstalled:
30
+ """A backend whose CLI is not on PATH.
31
+
32
+ Attributes:
33
+ binary: Name of the backend's CLI executable.
34
+ install_hint: Suggested shell command to install the CLI.
35
+ """
36
+
37
+ binary: str
38
+ install_hint: str
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class BackendNotAuthenticated:
43
+ """A backend whose CLI is installed but not authenticated.
44
+
45
+ Attributes:
46
+ binary: Name of the backend's CLI executable on PATH.
47
+ """
48
+
49
+ binary: str
50
+
51
+
52
+ BackendStatus = BackendReady | BackendNotInstalled | BackendNotAuthenticated
53
+ """Result of `LlmBackend.check_status`: `BackendReady`, `BackendNotInstalled`, or `BackendNotAuthenticated`."""
54
+
55
+
56
+ class BackendUnavailable(RuntimeError):
57
+ """Raised when no backend is ready (installed and authenticated)."""
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class Invocation:
62
+ """A built CLI invocation: argv, optional stdin, and where to read the result.
63
+
64
+ Attributes:
65
+ argv: The argv list to execute.
66
+ stdin: Prompt text delivered over stdin, or `None` when delivered inline.
67
+ result_path: File the backend writes its final message to; when set, the
68
+ result is read from this file instead of stdout.
69
+ cleanup_paths: Temp files to remove once the invocation completes.
70
+ """
71
+
72
+ argv: list[str]
73
+ stdin: str | None = None
74
+ result_path: str | None = None
75
+ cleanup_paths: tuple[str, ...] = ()
76
+
77
+
78
+ class LlmBackend(ABC):
79
+ """Abstract interface for an LLM CLI backend.
80
+
81
+ Concrete backends map abstract model sizes to provider-specific model names
82
+ and encapsulate how to invoke the provider's CLI and parse the raw response.
83
+
84
+ Attributes:
85
+ models: Mapping from abstract model size to the provider's model name.
86
+ """
87
+
88
+ models: ClassVar[dict[TModel, str]]
89
+ binary: ClassVar[str]
90
+ install_hint: ClassVar[str]
91
+
92
+ @abstractmethod
93
+ def build_command(self, model: str, schema_path: str | None, agent: bool) -> list[str]:
94
+ """Build the CLI argv for a single invocation (prompt delivered via stdin).
95
+
96
+ Args:
97
+ model: Provider-specific model name.
98
+ schema_path: Schema argument for structured output, or `None`.
99
+ agent: Whether the invocation may use tools / agent capabilities.
100
+
101
+ Returns:
102
+ The argv list to execute.
103
+ """
104
+
105
+ @abstractmethod
106
+ def parse_response(self, raw: str, response_model: type[BaseModel] | None) -> str | BaseModel:
107
+ """Parse raw CLI stdout into text or a validated model.
108
+
109
+ Args:
110
+ raw: Raw stdout from the backend CLI.
111
+ response_model: Model to validate against, or `None` for raw text.
112
+
113
+ Returns:
114
+ `raw` when `response_model` is `None`, else a validated instance.
115
+ """
116
+
117
+ @abstractmethod
118
+ def env(self) -> dict[str, str]:
119
+ """Return extra environment variables for the CLI invocation, merged over the inherited environment."""
120
+
121
+ def check_status(self, *, timeout: int = 10) -> BackendStatus:
122
+ """Check whether this backend's CLI is installed and authenticated.
123
+
124
+ Args:
125
+ timeout: Seconds to wait for the authentication probe.
126
+
127
+ Returns:
128
+ `BackendReady` when authenticated, `BackendNotInstalled` when the CLI
129
+ is not on PATH, else `BackendNotAuthenticated`.
130
+
131
+ Raises:
132
+ subprocess.TimeoutExpired: If `is_authenticated` exceeds `timeout`.
133
+ """
134
+ if not shutil.which(self.binary):
135
+ return BackendNotInstalled(binary=self.binary, install_hint=self.install_hint)
136
+ if self.is_authenticated(timeout=timeout):
137
+ return BackendReady(binary=self.binary)
138
+ return BackendNotAuthenticated(binary=self.binary)
139
+
140
+ @abstractmethod
141
+ def is_authenticated(self, *, timeout: int) -> bool:
142
+ """Probe whether the CLI holds valid credentials for its provider.
143
+
144
+ "Authenticated" means the CLI reports an active login session for the
145
+ provider, not merely that the executable is present on PATH.
146
+
147
+ Args:
148
+ timeout: Seconds to wait for the credential probe.
149
+
150
+ Returns:
151
+ `True` when the CLI reports an authenticated session.
152
+ """
153
+
154
+ def schema_for(self, model: type[BaseModel]) -> str:
155
+ """Serialize a Pydantic model into the JSON-schema string this backend's CLI expects.
156
+
157
+ The default emits the model's plain JSON schema; provider backends
158
+ override to apply their SDK's strict-schema transform.
159
+
160
+ Args:
161
+ model: The Pydantic model describing the structured output.
162
+
163
+ Returns:
164
+ A JSON-schema string suitable for this backend's structured-output argument.
165
+ """
166
+ return json.dumps(model.model_json_schema())
167
+
168
+ def invocation(self, prompt: str, *, model: str, schema_path: str | None, agent: bool) -> Invocation:
169
+ """Build the argv, stdin, and result source for a single invocation.
170
+
171
+ The default delivers the prompt over stdin and reads the result from
172
+ stdout; subclasses override to deliver the prompt inline or to read the
173
+ result from a file.
174
+
175
+ Args:
176
+ prompt: The prompt text to deliver to the CLI.
177
+ model: Provider-specific model name.
178
+ schema_path: Schema argument for structured output, or `None`.
179
+ agent: Whether the invocation may use tools / agent capabilities.
180
+
181
+ Returns:
182
+ An `Invocation` carrying the argv, stdin text, and result source.
183
+ """
184
+ return Invocation(self.build_command(model, schema_path, agent), prompt)