driangle-agentrunner 0.0.1__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.
- driangle_agentrunner-0.0.1/.gitignore +15 -0
- driangle_agentrunner-0.0.1/PKG-INFO +219 -0
- driangle_agentrunner-0.0.1/README.md +201 -0
- driangle_agentrunner-0.0.1/channel/pyproject.toml +24 -0
- driangle_agentrunner-0.0.1/channel/src/agentrunner_channel/__init__.py +41 -0
- driangle_agentrunner-0.0.1/channel/src/agentrunner_channel/py.typed +0 -0
- driangle_agentrunner-0.0.1/channel/tests/test_binary.py +19 -0
- driangle_agentrunner-0.0.1/pyproject.toml +43 -0
- driangle_agentrunner-0.0.1/src/agentrunner/__init__.py +35 -0
- driangle_agentrunner-0.0.1/src/agentrunner/claudecode/__init__.py +11 -0
- driangle_agentrunner-0.0.1/src/agentrunner/claudecode/args.py +51 -0
- driangle_agentrunner-0.0.1/src/agentrunner/claudecode/mapping.py +59 -0
- driangle_agentrunner-0.0.1/src/agentrunner/claudecode/options.py +33 -0
- driangle_agentrunner-0.0.1/src/agentrunner/claudecode/parser.py +152 -0
- driangle_agentrunner-0.0.1/src/agentrunner/claudecode/process.py +68 -0
- driangle_agentrunner-0.0.1/src/agentrunner/claudecode/runner.py +270 -0
- driangle_agentrunner-0.0.1/src/agentrunner/claudecode/types.py +131 -0
- driangle_agentrunner-0.0.1/src/agentrunner/claudecode/version.py +47 -0
- driangle_agentrunner-0.0.1/src/agentrunner/errors.py +36 -0
- driangle_agentrunner-0.0.1/src/agentrunner/ollama/__init__.py +14 -0
- driangle_agentrunner-0.0.1/src/agentrunner/ollama/accessors.py +22 -0
- driangle_agentrunner-0.0.1/src/agentrunner/ollama/options.py +41 -0
- driangle_agentrunner-0.0.1/src/agentrunner/ollama/runner.py +327 -0
- driangle_agentrunner-0.0.1/src/agentrunner/ollama/types.py +87 -0
- driangle_agentrunner-0.0.1/src/agentrunner/types.py +188 -0
- driangle_agentrunner-0.0.1/tests/__init__.py +0 -0
- driangle_agentrunner-0.0.1/tests/claudecode/__init__.py +0 -0
- driangle_agentrunner-0.0.1/tests/claudecode/test_args.py +105 -0
- driangle_agentrunner-0.0.1/tests/claudecode/test_mapping.py +32 -0
- driangle_agentrunner-0.0.1/tests/claudecode/test_parser.py +199 -0
- driangle_agentrunner-0.0.1/tests/claudecode/test_runner.py +657 -0
- driangle_agentrunner-0.0.1/tests/claudecode/test_version.py +25 -0
- driangle_agentrunner-0.0.1/tests/ollama/__init__.py +0 -0
- driangle_agentrunner-0.0.1/tests/ollama/test_runner.py +342 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
settings.local.json
|
|
2
|
+
node_modules
|
|
3
|
+
dist
|
|
4
|
+
docs/.vitepress/dist
|
|
5
|
+
docs/.vitepress/cache
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.pyc
|
|
8
|
+
|
|
9
|
+
# Compiled Go binaries
|
|
10
|
+
examples/go/claudecode/claudecode
|
|
11
|
+
examples/go/ollama/ollama
|
|
12
|
+
|
|
13
|
+
# Platform binaries (placed by CI before publishing)
|
|
14
|
+
npm/*/bin/
|
|
15
|
+
python/channel/src/agentrunner_channel/bin/
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: driangle-agentrunner
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python library for programmatically invoking AI coding agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/driangle/agentrunner
|
|
6
|
+
Project-URL: Repository, https://github.com/driangle/agentrunner
|
|
7
|
+
Project-URL: Issues, https://github.com/driangle/agentrunner/issues
|
|
8
|
+
Author: driangle
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Provides-Extra: channel
|
|
12
|
+
Requires-Dist: agentrunner-channel>=0.0.1; extra == 'channel'
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# agentrunner (Python)
|
|
20
|
+
|
|
21
|
+
Python library for programmatically invoking AI coding agents. Part of the [agentrunner](../) monorepo.
|
|
22
|
+
|
|
23
|
+
## Supported CLIs
|
|
24
|
+
|
|
25
|
+
| Runner | CLI Version | Status |
|
|
26
|
+
|-------------|-------------|--------|
|
|
27
|
+
| Claude Code | >= 1.0.12 | ✅ |
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Python >= 3.11
|
|
32
|
+
- Claude Code CLI >= 1.0.12
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install agentrunner
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
from agentrunner.claudecode import create_claude_runner, ClaudeRunOptions
|
|
45
|
+
|
|
46
|
+
runner = create_claude_runner()
|
|
47
|
+
|
|
48
|
+
async def main():
|
|
49
|
+
# Simple run
|
|
50
|
+
result = await runner.run("What files are in this directory?", ClaudeRunOptions(
|
|
51
|
+
working_dir="/path/to/project",
|
|
52
|
+
skip_permissions=True,
|
|
53
|
+
))
|
|
54
|
+
print(result.text)
|
|
55
|
+
|
|
56
|
+
# Streaming
|
|
57
|
+
stream = await runner.run_stream("Explain this codebase")
|
|
58
|
+
async for message in stream:
|
|
59
|
+
print(message.type, message.raw)
|
|
60
|
+
|
|
61
|
+
asyncio.run(main())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## API
|
|
65
|
+
|
|
66
|
+
### `create_claude_runner(config?)`
|
|
67
|
+
|
|
68
|
+
Creates a runner for the Claude Code CLI.
|
|
69
|
+
|
|
70
|
+
**Config options (`ClaudeRunnerConfig`):**
|
|
71
|
+
|
|
72
|
+
| Field | Type | Default | Description |
|
|
73
|
+
|----------|------------|------------|--------------------------------------|
|
|
74
|
+
| `binary` | `str` | `"claude"` | CLI binary name or path |
|
|
75
|
+
| `spawn` | `SpawnFn` | — | Custom spawn function (for testing) |
|
|
76
|
+
| `logger` | `Logger` | — | Logger for debug output (opt-in) |
|
|
77
|
+
|
|
78
|
+
### `runner.run(prompt, options?)`
|
|
79
|
+
|
|
80
|
+
Execute a prompt and return the final `Result`.
|
|
81
|
+
|
|
82
|
+
### `runner.run_stream(prompt, options?)`
|
|
83
|
+
|
|
84
|
+
Execute a prompt and stream messages as they arrive. Returns `AsyncIterable[Message]`.
|
|
85
|
+
|
|
86
|
+
### `runner.start(prompt, options?)`
|
|
87
|
+
|
|
88
|
+
Launch an agent process and return a `Session` for full lifecycle control.
|
|
89
|
+
|
|
90
|
+
### Run Options
|
|
91
|
+
|
|
92
|
+
Common options (`RunOptions`):
|
|
93
|
+
|
|
94
|
+
| Field | Type | Description |
|
|
95
|
+
|-----------------------|------------------|--------------------------------------|
|
|
96
|
+
| `model` | `str` | Model name or alias |
|
|
97
|
+
| `system_prompt` | `str` | System prompt override |
|
|
98
|
+
| `append_system_prompt`| `str` | Appended to default system prompt |
|
|
99
|
+
| `working_dir` | `str` | Working directory for subprocess |
|
|
100
|
+
| `env` | `dict[str, str]` | Additional environment variables |
|
|
101
|
+
| `max_turns` | `int` | Maximum agentic turns |
|
|
102
|
+
| `timeout` | `float` | Timeout in milliseconds |
|
|
103
|
+
| `skip_permissions` | `bool` | Skip permission prompts |
|
|
104
|
+
|
|
105
|
+
Claude-specific options (`ClaudeRunOptions` extends `RunOptions`):
|
|
106
|
+
|
|
107
|
+
| Field | Type | Description |
|
|
108
|
+
|--------------------------|-------------|------------------------------------|
|
|
109
|
+
| `allowed_tools` | `list[str]` | Tools the agent may use |
|
|
110
|
+
| `disallowed_tools` | `list[str]` | Tools the agent may not use |
|
|
111
|
+
| `mcp_config` | `str` | Path to MCP server config |
|
|
112
|
+
| `json_schema` | `str` | JSON Schema for structured output |
|
|
113
|
+
| `max_budget_usd` | `float` | Cost limit in USD |
|
|
114
|
+
| `resume` | `str` | Session ID to resume |
|
|
115
|
+
| `continue_session` | `bool` | Continue most recent session |
|
|
116
|
+
| `session_id` | `str` | Specific session ID |
|
|
117
|
+
| `include_partial_messages`| `bool` | Stream partial/incremental messages|
|
|
118
|
+
| `on_message` | `callable` | Callback for each streamed message |
|
|
119
|
+
|
|
120
|
+
### Result
|
|
121
|
+
|
|
122
|
+
| Field | Type | Description |
|
|
123
|
+
|--------------|---------|---------------------------------|
|
|
124
|
+
| `text` | `str` | Final response text |
|
|
125
|
+
| `is_error` | `bool` | Whether the run ended in error |
|
|
126
|
+
| `exit_code` | `int` | Process exit code |
|
|
127
|
+
| `usage` | `Usage` | Token counts |
|
|
128
|
+
| `cost_usd` | `float` | Estimated cost in USD |
|
|
129
|
+
| `duration_ms`| `float` | Wall-clock duration in ms |
|
|
130
|
+
| `session_id` | `str` | Session ID for resumption |
|
|
131
|
+
|
|
132
|
+
### Session
|
|
133
|
+
|
|
134
|
+
| Attribute | Type | Description |
|
|
135
|
+
|------------|--------------------------|---------------------------------------|
|
|
136
|
+
| `messages` | `AsyncIterable[Message]` | Iterate messages as they arrive |
|
|
137
|
+
| `result` | `Future[Result]` | Resolves when the agent finishes |
|
|
138
|
+
| `abort()` | — | Terminate the agent process |
|
|
139
|
+
| `send()` | — | Reserved (raises `RuntimeError`) |
|
|
140
|
+
|
|
141
|
+
### Error Classes
|
|
142
|
+
|
|
143
|
+
All errors extend `RunnerError`:
|
|
144
|
+
|
|
145
|
+
- `NotFoundError` — CLI binary not found
|
|
146
|
+
- `TimeoutError` — execution timed out
|
|
147
|
+
- `NonZeroExitError` — CLI exited with non-zero code (has `.exit_code`)
|
|
148
|
+
- `ParseError` — failed to parse CLI output
|
|
149
|
+
- `CancelledError` — execution cancelled
|
|
150
|
+
- `NoResultError` — stream ended without a result message
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from agentrunner import TimeoutError
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
await runner.run("complex task", ClaudeRunOptions(timeout=30_000))
|
|
157
|
+
except TimeoutError:
|
|
158
|
+
print("Timed out!")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Usage Examples
|
|
162
|
+
|
|
163
|
+
### Session Resume
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
# First run — capture the session ID.
|
|
167
|
+
result = await runner.run("Set up the project structure")
|
|
168
|
+
session_id = result.session_id
|
|
169
|
+
|
|
170
|
+
# Resume the same session later.
|
|
171
|
+
result = await runner.run("Now add tests", ClaudeRunOptions(resume=session_id))
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Session Object
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
session = runner.start("Explain this code", ClaudeRunOptions(max_turns=1, timeout=30_000))
|
|
178
|
+
|
|
179
|
+
async for msg in session.messages:
|
|
180
|
+
print(f"[{msg.type}] {msg.raw[:80]}")
|
|
181
|
+
|
|
182
|
+
result = await session.result
|
|
183
|
+
print(f"Response: {result.text}")
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Streaming with Partial Messages
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from agentrunner.claudecode import parse
|
|
190
|
+
|
|
191
|
+
stream = await runner.run_stream("List fun facts", ClaudeRunOptions(
|
|
192
|
+
include_partial_messages=True,
|
|
193
|
+
))
|
|
194
|
+
async for msg in stream:
|
|
195
|
+
if msg.type == "assistant":
|
|
196
|
+
parsed = parse(msg.raw)
|
|
197
|
+
if parsed.type == "stream_event":
|
|
198
|
+
import json
|
|
199
|
+
raw = json.loads(msg.raw)
|
|
200
|
+
delta = raw.get("event", {}).get("delta", {})
|
|
201
|
+
if delta.get("type") == "text_delta":
|
|
202
|
+
print(delta["text"], end="", flush=True)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
cd python
|
|
209
|
+
pip install -e ".[dev]" # install with dev dependencies
|
|
210
|
+
ruff check src/ tests/ # lint
|
|
211
|
+
python -m pytest # run tests
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Or from the repo root:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
make check-python # build + lint + test
|
|
218
|
+
make check # all libraries
|
|
219
|
+
```
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# agentrunner (Python)
|
|
2
|
+
|
|
3
|
+
Python library for programmatically invoking AI coding agents. Part of the [agentrunner](../) monorepo.
|
|
4
|
+
|
|
5
|
+
## Supported CLIs
|
|
6
|
+
|
|
7
|
+
| Runner | CLI Version | Status |
|
|
8
|
+
|-------------|-------------|--------|
|
|
9
|
+
| Claude Code | >= 1.0.12 | ✅ |
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Python >= 3.11
|
|
14
|
+
- Claude Code CLI >= 1.0.12
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install agentrunner
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import asyncio
|
|
26
|
+
from agentrunner.claudecode import create_claude_runner, ClaudeRunOptions
|
|
27
|
+
|
|
28
|
+
runner = create_claude_runner()
|
|
29
|
+
|
|
30
|
+
async def main():
|
|
31
|
+
# Simple run
|
|
32
|
+
result = await runner.run("What files are in this directory?", ClaudeRunOptions(
|
|
33
|
+
working_dir="/path/to/project",
|
|
34
|
+
skip_permissions=True,
|
|
35
|
+
))
|
|
36
|
+
print(result.text)
|
|
37
|
+
|
|
38
|
+
# Streaming
|
|
39
|
+
stream = await runner.run_stream("Explain this codebase")
|
|
40
|
+
async for message in stream:
|
|
41
|
+
print(message.type, message.raw)
|
|
42
|
+
|
|
43
|
+
asyncio.run(main())
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
### `create_claude_runner(config?)`
|
|
49
|
+
|
|
50
|
+
Creates a runner for the Claude Code CLI.
|
|
51
|
+
|
|
52
|
+
**Config options (`ClaudeRunnerConfig`):**
|
|
53
|
+
|
|
54
|
+
| Field | Type | Default | Description |
|
|
55
|
+
|----------|------------|------------|--------------------------------------|
|
|
56
|
+
| `binary` | `str` | `"claude"` | CLI binary name or path |
|
|
57
|
+
| `spawn` | `SpawnFn` | — | Custom spawn function (for testing) |
|
|
58
|
+
| `logger` | `Logger` | — | Logger for debug output (opt-in) |
|
|
59
|
+
|
|
60
|
+
### `runner.run(prompt, options?)`
|
|
61
|
+
|
|
62
|
+
Execute a prompt and return the final `Result`.
|
|
63
|
+
|
|
64
|
+
### `runner.run_stream(prompt, options?)`
|
|
65
|
+
|
|
66
|
+
Execute a prompt and stream messages as they arrive. Returns `AsyncIterable[Message]`.
|
|
67
|
+
|
|
68
|
+
### `runner.start(prompt, options?)`
|
|
69
|
+
|
|
70
|
+
Launch an agent process and return a `Session` for full lifecycle control.
|
|
71
|
+
|
|
72
|
+
### Run Options
|
|
73
|
+
|
|
74
|
+
Common options (`RunOptions`):
|
|
75
|
+
|
|
76
|
+
| Field | Type | Description |
|
|
77
|
+
|-----------------------|------------------|--------------------------------------|
|
|
78
|
+
| `model` | `str` | Model name or alias |
|
|
79
|
+
| `system_prompt` | `str` | System prompt override |
|
|
80
|
+
| `append_system_prompt`| `str` | Appended to default system prompt |
|
|
81
|
+
| `working_dir` | `str` | Working directory for subprocess |
|
|
82
|
+
| `env` | `dict[str, str]` | Additional environment variables |
|
|
83
|
+
| `max_turns` | `int` | Maximum agentic turns |
|
|
84
|
+
| `timeout` | `float` | Timeout in milliseconds |
|
|
85
|
+
| `skip_permissions` | `bool` | Skip permission prompts |
|
|
86
|
+
|
|
87
|
+
Claude-specific options (`ClaudeRunOptions` extends `RunOptions`):
|
|
88
|
+
|
|
89
|
+
| Field | Type | Description |
|
|
90
|
+
|--------------------------|-------------|------------------------------------|
|
|
91
|
+
| `allowed_tools` | `list[str]` | Tools the agent may use |
|
|
92
|
+
| `disallowed_tools` | `list[str]` | Tools the agent may not use |
|
|
93
|
+
| `mcp_config` | `str` | Path to MCP server config |
|
|
94
|
+
| `json_schema` | `str` | JSON Schema for structured output |
|
|
95
|
+
| `max_budget_usd` | `float` | Cost limit in USD |
|
|
96
|
+
| `resume` | `str` | Session ID to resume |
|
|
97
|
+
| `continue_session` | `bool` | Continue most recent session |
|
|
98
|
+
| `session_id` | `str` | Specific session ID |
|
|
99
|
+
| `include_partial_messages`| `bool` | Stream partial/incremental messages|
|
|
100
|
+
| `on_message` | `callable` | Callback for each streamed message |
|
|
101
|
+
|
|
102
|
+
### Result
|
|
103
|
+
|
|
104
|
+
| Field | Type | Description |
|
|
105
|
+
|--------------|---------|---------------------------------|
|
|
106
|
+
| `text` | `str` | Final response text |
|
|
107
|
+
| `is_error` | `bool` | Whether the run ended in error |
|
|
108
|
+
| `exit_code` | `int` | Process exit code |
|
|
109
|
+
| `usage` | `Usage` | Token counts |
|
|
110
|
+
| `cost_usd` | `float` | Estimated cost in USD |
|
|
111
|
+
| `duration_ms`| `float` | Wall-clock duration in ms |
|
|
112
|
+
| `session_id` | `str` | Session ID for resumption |
|
|
113
|
+
|
|
114
|
+
### Session
|
|
115
|
+
|
|
116
|
+
| Attribute | Type | Description |
|
|
117
|
+
|------------|--------------------------|---------------------------------------|
|
|
118
|
+
| `messages` | `AsyncIterable[Message]` | Iterate messages as they arrive |
|
|
119
|
+
| `result` | `Future[Result]` | Resolves when the agent finishes |
|
|
120
|
+
| `abort()` | — | Terminate the agent process |
|
|
121
|
+
| `send()` | — | Reserved (raises `RuntimeError`) |
|
|
122
|
+
|
|
123
|
+
### Error Classes
|
|
124
|
+
|
|
125
|
+
All errors extend `RunnerError`:
|
|
126
|
+
|
|
127
|
+
- `NotFoundError` — CLI binary not found
|
|
128
|
+
- `TimeoutError` — execution timed out
|
|
129
|
+
- `NonZeroExitError` — CLI exited with non-zero code (has `.exit_code`)
|
|
130
|
+
- `ParseError` — failed to parse CLI output
|
|
131
|
+
- `CancelledError` — execution cancelled
|
|
132
|
+
- `NoResultError` — stream ended without a result message
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from agentrunner import TimeoutError
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
await runner.run("complex task", ClaudeRunOptions(timeout=30_000))
|
|
139
|
+
except TimeoutError:
|
|
140
|
+
print("Timed out!")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Usage Examples
|
|
144
|
+
|
|
145
|
+
### Session Resume
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
# First run — capture the session ID.
|
|
149
|
+
result = await runner.run("Set up the project structure")
|
|
150
|
+
session_id = result.session_id
|
|
151
|
+
|
|
152
|
+
# Resume the same session later.
|
|
153
|
+
result = await runner.run("Now add tests", ClaudeRunOptions(resume=session_id))
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Session Object
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
session = runner.start("Explain this code", ClaudeRunOptions(max_turns=1, timeout=30_000))
|
|
160
|
+
|
|
161
|
+
async for msg in session.messages:
|
|
162
|
+
print(f"[{msg.type}] {msg.raw[:80]}")
|
|
163
|
+
|
|
164
|
+
result = await session.result
|
|
165
|
+
print(f"Response: {result.text}")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Streaming with Partial Messages
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from agentrunner.claudecode import parse
|
|
172
|
+
|
|
173
|
+
stream = await runner.run_stream("List fun facts", ClaudeRunOptions(
|
|
174
|
+
include_partial_messages=True,
|
|
175
|
+
))
|
|
176
|
+
async for msg in stream:
|
|
177
|
+
if msg.type == "assistant":
|
|
178
|
+
parsed = parse(msg.raw)
|
|
179
|
+
if parsed.type == "stream_event":
|
|
180
|
+
import json
|
|
181
|
+
raw = json.loads(msg.raw)
|
|
182
|
+
delta = raw.get("event", {}).get("delta", {})
|
|
183
|
+
if delta.get("type") == "text_delta":
|
|
184
|
+
print(delta["text"], end="", flush=True)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Development
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
cd python
|
|
191
|
+
pip install -e ".[dev]" # install with dev dependencies
|
|
192
|
+
ruff check src/ tests/ # lint
|
|
193
|
+
python -m pytest # run tests
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Or from the repo root:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
make check-python # build + lint + test
|
|
200
|
+
make check # all libraries
|
|
201
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentrunner-channel"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "agentrunner-channel binary distribution"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
dev = [
|
|
14
|
+
"pytest>=8.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[tool.hatch.build.targets.wheel]
|
|
18
|
+
packages = ["src/agentrunner_channel"]
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build]
|
|
21
|
+
artifacts = ["src/agentrunner_channel/bin/*"]
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Resolve the agentrunner-channel binary path."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def binary_path() -> str:
|
|
10
|
+
"""Return the path to the agentrunner-channel binary.
|
|
11
|
+
|
|
12
|
+
Resolution order:
|
|
13
|
+
1. AGENTRUNNER_CHANNEL_BIN environment variable
|
|
14
|
+
2. Bundled binary in this package's bin/ directory
|
|
15
|
+
3. agentrunner-channel on $PATH
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
FileNotFoundError: if the binary cannot be found.
|
|
19
|
+
"""
|
|
20
|
+
env_path = os.environ.get("AGENTRUNNER_CHANNEL_BIN")
|
|
21
|
+
if env_path:
|
|
22
|
+
return env_path
|
|
23
|
+
|
|
24
|
+
name = (
|
|
25
|
+
"agentrunner-channel.exe"
|
|
26
|
+
if platform.system() == "Windows"
|
|
27
|
+
else "agentrunner-channel"
|
|
28
|
+
)
|
|
29
|
+
bundled = Path(__file__).parent / "bin" / name
|
|
30
|
+
if bundled.exists():
|
|
31
|
+
return str(bundled)
|
|
32
|
+
|
|
33
|
+
found = shutil.which("agentrunner-channel")
|
|
34
|
+
if found:
|
|
35
|
+
return found
|
|
36
|
+
|
|
37
|
+
raise FileNotFoundError(
|
|
38
|
+
"agentrunner-channel binary not found. "
|
|
39
|
+
"Install the agentrunner-channel package for your platform, "
|
|
40
|
+
"add it to $PATH, or set AGENTRUNNER_CHANNEL_BIN."
|
|
41
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Tests for channel binary resolution."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from agentrunner_channel import binary_path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestBinaryPath:
|
|
11
|
+
def test_env_override(self, monkeypatch):
|
|
12
|
+
monkeypatch.setenv("AGENTRUNNER_CHANNEL_BIN", "/custom/agentrunner-channel")
|
|
13
|
+
assert binary_path() == "/custom/agentrunner-channel"
|
|
14
|
+
|
|
15
|
+
def test_not_found_raises(self, monkeypatch):
|
|
16
|
+
monkeypatch.delenv("AGENTRUNNER_CHANNEL_BIN", raising=False)
|
|
17
|
+
monkeypatch.setattr("shutil.which", lambda _: None)
|
|
18
|
+
with pytest.raises(FileNotFoundError, match="agentrunner-channel binary not found"):
|
|
19
|
+
binary_path()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "driangle-agentrunner"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Python library for programmatically invoking AI coding agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "driangle" },
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://github.com/driangle/agentrunner"
|
|
18
|
+
Repository = "https://github.com/driangle/agentrunner"
|
|
19
|
+
Issues = "https://github.com/driangle/agentrunner/issues"
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
channel = [
|
|
23
|
+
"agentrunner-channel>=0.0.1",
|
|
24
|
+
]
|
|
25
|
+
dev = [
|
|
26
|
+
"pytest>=8.0",
|
|
27
|
+
"pytest-asyncio>=0.24",
|
|
28
|
+
"ruff>=0.8",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/agentrunner"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
asyncio_mode = "auto"
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
target-version = "py311"
|
|
40
|
+
line-length = 100
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint]
|
|
43
|
+
select = ["E", "F", "I", "W"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""agentrunner — Python library for programmatically invoking AI coding agents."""
|
|
2
|
+
|
|
3
|
+
from .claudecode import ClaudeRunner, ClaudeRunOptions
|
|
4
|
+
from .errors import (
|
|
5
|
+
CancelledError,
|
|
6
|
+
NonZeroExitError,
|
|
7
|
+
NoResultError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
ParseError,
|
|
10
|
+
RunnerError,
|
|
11
|
+
TimeoutError,
|
|
12
|
+
)
|
|
13
|
+
from .ollama import OllamaRunner, OllamaRunnerConfig, OllamaRunOptions
|
|
14
|
+
from .types import Message, Result, Runner, RunOptions, Session, Usage
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"CancelledError",
|
|
18
|
+
"ClaudeRunner",
|
|
19
|
+
"ClaudeRunOptions",
|
|
20
|
+
"Message",
|
|
21
|
+
"NoResultError",
|
|
22
|
+
"NonZeroExitError",
|
|
23
|
+
"NotFoundError",
|
|
24
|
+
"OllamaRunner",
|
|
25
|
+
"OllamaRunnerConfig",
|
|
26
|
+
"OllamaRunOptions",
|
|
27
|
+
"ParseError",
|
|
28
|
+
"Result",
|
|
29
|
+
"RunOptions",
|
|
30
|
+
"Runner",
|
|
31
|
+
"RunnerError",
|
|
32
|
+
"Session",
|
|
33
|
+
"TimeoutError",
|
|
34
|
+
"Usage",
|
|
35
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Build CLI arguments from prompt and options."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .options import ClaudeRunOptions
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_args(prompt: str, options: ClaudeRunOptions | None = None) -> list[str]:
|
|
9
|
+
"""Build CLI arguments from prompt and options."""
|
|
10
|
+
args = ["--print", "--output-format", "stream-json", "--verbose"]
|
|
11
|
+
|
|
12
|
+
if options is None:
|
|
13
|
+
args.extend(["--", prompt])
|
|
14
|
+
return args
|
|
15
|
+
|
|
16
|
+
# Common options.
|
|
17
|
+
if options.model:
|
|
18
|
+
args.extend(["--model", options.model])
|
|
19
|
+
if options.system_prompt:
|
|
20
|
+
args.extend(["--system-prompt", options.system_prompt])
|
|
21
|
+
if options.append_system_prompt:
|
|
22
|
+
args.extend(["--append-system-prompt", options.append_system_prompt])
|
|
23
|
+
if options.max_turns is not None and options.max_turns > 0:
|
|
24
|
+
args.extend(["--max-turns", str(options.max_turns)])
|
|
25
|
+
if options.skip_permissions:
|
|
26
|
+
args.append("--dangerously-skip-permissions")
|
|
27
|
+
|
|
28
|
+
# Claude-specific options.
|
|
29
|
+
if options.allowed_tools:
|
|
30
|
+
for tool in options.allowed_tools:
|
|
31
|
+
args.extend(["--allowedTools", tool])
|
|
32
|
+
if options.disallowed_tools:
|
|
33
|
+
for tool in options.disallowed_tools:
|
|
34
|
+
args.extend(["--disallowedTools", tool])
|
|
35
|
+
if options.mcp_config:
|
|
36
|
+
args.extend(["--mcp-config", options.mcp_config])
|
|
37
|
+
if options.json_schema:
|
|
38
|
+
args.extend(["--json-schema", options.json_schema])
|
|
39
|
+
if options.max_budget_usd is not None and options.max_budget_usd > 0:
|
|
40
|
+
args.extend(["--max-budget-usd", str(options.max_budget_usd)])
|
|
41
|
+
if options.resume:
|
|
42
|
+
args.extend(["--resume", options.resume])
|
|
43
|
+
if options.continue_session:
|
|
44
|
+
args.append("--continue")
|
|
45
|
+
if options.session_id:
|
|
46
|
+
args.extend(["--session-id", options.session_id])
|
|
47
|
+
if options.include_partial_messages:
|
|
48
|
+
args.append("--include-partial-messages")
|
|
49
|
+
|
|
50
|
+
args.extend(["--", prompt])
|
|
51
|
+
return args
|