agentshim 0.3.1__tar.gz → 0.5.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.
- agentshim-0.5.0/.github/workflows/ci.yml +41 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/PKG-INFO +161 -7
- {agentshim-0.3.1 → agentshim-0.5.0}/README.md +160 -6
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/__init__.py +20 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/base.py +15 -30
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/claude/agent.py +11 -40
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/claude/events.py +1 -34
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/cli_agent.py +62 -176
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/codex/agent.py +13 -43
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/codex/events.py +1 -33
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/copilot/agent.py +23 -50
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/copilot/events.py +1 -47
- agentshim-0.5.0/agentshim/events.py +263 -0
- agentshim-0.5.0/agentshim/executor.py +319 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/gemini/agent.py +14 -87
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/gemini/events.py +1 -26
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/llm_client.py +4 -15
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/opencode/agent.py +23 -39
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/opencode/events.py +2 -35
- {agentshim-0.3.1 → agentshim-0.5.0}/pyproject.toml +1 -1
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/cli_agent/test_agent_cli_cleanup.py +0 -1
- agentshim-0.5.0/tests/unit/cli_agent/test_command_executor.py +245 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/llm/test_claude_stream.py +25 -27
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_claude.py +5 -9
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_copilot.py +41 -11
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_copilot_fixtures.py +7 -7
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_event_parsing.py +0 -48
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_resume.py +0 -6
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_coding_agent_facade.py +19 -7
- agentshim-0.5.0/tests/unit/test_event_handlers.py +83 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/uv.lock +1 -1
- agentshim-0.3.1/.github/workflows/ci.yml +0 -18
- agentshim-0.3.1/agentshim/events.py +0 -28
- agentshim-0.3.1/agentshim/trajectory.py +0 -168
- agentshim-0.3.1/tests/unit/test_coding_agent_recorder_default.py +0 -12
- {agentshim-0.3.1 → agentshim-0.5.0}/.github/workflows/publish.yml +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/.gitignore +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/claude/__init__.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/claude/hooks/__init__.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/claude_events.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/codex/__init__.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/codex_events.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/copilot/__init__.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/copilot_events.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/gemini/__init__.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/gemini_events.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/mcp_config.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/opencode/__init__.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/opencode_events.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/py.typed +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/sandbox.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/subagent.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/usage.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/utils.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/fixtures/copilot/session_turn_1.jsonl +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/fixtures/copilot/session_turn_2_resumed.jsonl +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/fixtures/copilot/streaming_dedup.jsonl +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/fixtures/copilot/tool_and_usage.jsonl +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/cli_agent/test_check_cli.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/cli_agent/test_cli_prompt_passing.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/llm/conftest.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/llm/test_gemini_fixture.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/llm/test_gemini_stream.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_codex.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_mcp_unsupported.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_sandbox.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_cli_agent_usage.py +0 -0
- {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_mcp_config.py +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
lint:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: astral-sh/setup-uv@v6
|
|
16
|
+
with:
|
|
17
|
+
enable-cache: true
|
|
18
|
+
cache-dependency-glob: uv.lock
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.11"
|
|
22
|
+
- run: uv sync --locked --dev
|
|
23
|
+
- run: uv run ruff check .
|
|
24
|
+
|
|
25
|
+
test:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
strategy:
|
|
28
|
+
fail-fast: false
|
|
29
|
+
matrix:
|
|
30
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: astral-sh/setup-uv@v6
|
|
34
|
+
with:
|
|
35
|
+
enable-cache: true
|
|
36
|
+
cache-dependency-glob: uv.lock
|
|
37
|
+
- uses: actions/setup-python@v5
|
|
38
|
+
with:
|
|
39
|
+
python-version: ${{ matrix.python-version }}
|
|
40
|
+
- run: uv sync --locked --dev
|
|
41
|
+
- run: uv run pytest
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentshim
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Provider-agnostic coding-agent CLI shims
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: litellm>=1.0.0
|
|
@@ -22,10 +22,10 @@ prompting, session resumption, event parsing, or MCP configuration.
|
|
|
22
22
|
- a shared CLI agent abstraction with a provider registry
|
|
23
23
|
- adapters for Claude Code, Codex, Gemini, and Opencode
|
|
24
24
|
- stateful chat sessions that automatically resume provider-native threads
|
|
25
|
+
- injectable command executors for custom process launching or sandboxing
|
|
25
26
|
- MCP server config models for providers that support MCP
|
|
26
27
|
- sandbox settings helpers for Claude Code
|
|
27
28
|
- a lightweight LiteLLM client and subagent helper
|
|
28
|
-
- trajectory/usage helpers used by higher-level runtimes
|
|
29
29
|
|
|
30
30
|
## Install
|
|
31
31
|
|
|
@@ -81,7 +81,162 @@ reply = agent.generate("Write a short summary of this codebase.", cwd=".")
|
|
|
81
81
|
print(reply)
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
### 2.
|
|
84
|
+
### 2. Handle Agent Events
|
|
85
|
+
|
|
86
|
+
By default, `agentshim` prints provider events to the terminal through a
|
|
87
|
+
`ConsoleEventHandler`. That default is used only when you do not provide your
|
|
88
|
+
own event handler and `silent=False`.
|
|
89
|
+
|
|
90
|
+
If you pass `event_handler=...`, you take ownership of event handling. The
|
|
91
|
+
built-in console printer is not added implicitly, which avoids surprising
|
|
92
|
+
duplicate output.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from agentshim import CodingAgent
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class MyHandler:
|
|
99
|
+
def on_thinking(self, text: str) -> None:
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
def on_tool_call(self, tool: str, args=None) -> None:
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
def on_tool_result(
|
|
106
|
+
self,
|
|
107
|
+
tool: str,
|
|
108
|
+
stdout: str = "",
|
|
109
|
+
stderr: str = "",
|
|
110
|
+
exit_code: int | None = None,
|
|
111
|
+
duration: float | None = None,
|
|
112
|
+
) -> None:
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
def on_usage(self, usage: dict) -> None:
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
agent = CodingAgent(provider="claude", event_handler=MyHandler())
|
|
120
|
+
agent.generate("Inspect this repository.")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
To keep the default console output and add your own handler, compose them
|
|
124
|
+
explicitly:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from agentshim import CodingAgent, ConsoleEventHandler
|
|
128
|
+
|
|
129
|
+
agent = CodingAgent(
|
|
130
|
+
provider="claude",
|
|
131
|
+
event_handlers=[
|
|
132
|
+
ConsoleEventHandler(),
|
|
133
|
+
MyHandler(),
|
|
134
|
+
],
|
|
135
|
+
)
|
|
136
|
+
agent.generate("Inspect this repository.")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
You can also build the composition yourself:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from agentshim import CompositeEventHandler, ConsoleEventHandler
|
|
143
|
+
|
|
144
|
+
handler = CompositeEventHandler([ConsoleEventHandler(), MyHandler()])
|
|
145
|
+
agent = CodingAgent(provider="codex", event_handler=handler)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Use `silent=True` to suppress the default console handler when you have not
|
|
149
|
+
provided any handler:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
agent = CodingAgent(provider="claude")
|
|
153
|
+
reply = agent.generate("Return only the answer.", silent=True)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 3. Run Commands Through a Custom Executor
|
|
157
|
+
|
|
158
|
+
Provider classes accept an optional `executor=`. The executor controls binary
|
|
159
|
+
lookup, CLI validation, and streaming process execution while `agentshim` keeps
|
|
160
|
+
owning provider command construction, stdout/stderr parsing, session state, and
|
|
161
|
+
event emission.
|
|
162
|
+
|
|
163
|
+
This is useful when a caller needs to run the CLI somewhere other than the
|
|
164
|
+
current host process, for example through an existing container, remote shell,
|
|
165
|
+
or custom sandbox.
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from agentshim import (
|
|
169
|
+
CodexCodingAgent,
|
|
170
|
+
CommandHandle,
|
|
171
|
+
CommandRequest,
|
|
172
|
+
CommandResult,
|
|
173
|
+
CommandStreamSink,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class MyCommandHandle:
|
|
178
|
+
def terminate(self) -> None:
|
|
179
|
+
...
|
|
180
|
+
|
|
181
|
+
def kill(self) -> None:
|
|
182
|
+
...
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class MyExecutor:
|
|
186
|
+
def find_binary(self, binary_name: str, env: dict[str, str]) -> str:
|
|
187
|
+
# This value becomes request.argv[0]. Container or remote executors can
|
|
188
|
+
# return the binary name if lookup happens in the target runtime.
|
|
189
|
+
return binary_name
|
|
190
|
+
|
|
191
|
+
def check_binary(self, binary_path: str, env: dict[str, str], *, timeout: int) -> None:
|
|
192
|
+
# Raise RuntimeError if the target CLI is unavailable. No-op is fine
|
|
193
|
+
# when validation is not cheap or is handled by the runtime.
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def run(self, request: CommandRequest, sink: CommandStreamSink) -> CommandResult:
|
|
197
|
+
handle = MyCommandHandle()
|
|
198
|
+
sink.started(handle)
|
|
199
|
+
|
|
200
|
+
stdout = ""
|
|
201
|
+
stderr = ""
|
|
202
|
+
|
|
203
|
+
# Run request.argv in your target runtime, with request.stdin,
|
|
204
|
+
# request.cwd, request.env, and request.timeout. Stream each complete
|
|
205
|
+
# line as it arrives, preserving trailing newlines when present.
|
|
206
|
+
line = "streamed output\n"
|
|
207
|
+
stdout += line
|
|
208
|
+
sink.stdout(line)
|
|
209
|
+
|
|
210
|
+
return CommandResult(returncode=0, stdout=stdout, stderr=stderr)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
agent = CodexCodingAgent(executor=MyExecutor())
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The default `HostCommandExecutor` preserves the normal local `subprocess`
|
|
217
|
+
behavior. `CodingAgent(provider=..., executor=...)` forwards the same executor
|
|
218
|
+
to the selected provider.
|
|
219
|
+
|
|
220
|
+
Executor contract:
|
|
221
|
+
|
|
222
|
+
- `CommandRequest.argv` is the complete provider CLI command. `argv[0]` is the
|
|
223
|
+
value returned by `find_binary`.
|
|
224
|
+
- `CommandRequest.stdin` is the prompt text to write to the command's standard
|
|
225
|
+
input, then stdin should be closed.
|
|
226
|
+
- `CommandRequest.cwd`, `env`, and `timeout` should be honored by the executor.
|
|
227
|
+
- Call `sink.started(handle)` once after the command starts. The handle only
|
|
228
|
+
needs `terminate()` and `kill()`.
|
|
229
|
+
- Call `sink.stdout(line)` and `sink.stderr(line)` as output is produced. Lines
|
|
230
|
+
should include trailing newlines when the underlying stream provided them.
|
|
231
|
+
- Return `CommandResult(returncode, stdout, stderr)` after the command exits.
|
|
232
|
+
The returned text should match what was streamed through the sink.
|
|
233
|
+
|
|
234
|
+
If you were using the pre-0.5 executor preview, replace
|
|
235
|
+
`run_streaming(cmd, ..., on_stdout, on_stderr, on_process_started)` with
|
|
236
|
+
`run(request, sink)`. The parser/event APIs remain internal; custom executors
|
|
237
|
+
only provide a stable command runtime.
|
|
238
|
+
|
|
239
|
+
### 4. Instantiate a Specific Provider Directly
|
|
85
240
|
|
|
86
241
|
If you already know which backend you want, construct the provider class
|
|
87
242
|
yourself.
|
|
@@ -103,7 +258,7 @@ The bundled provider classes are:
|
|
|
103
258
|
- `GeminiCodingAgent`
|
|
104
259
|
- `OpencodeCodingAgent`
|
|
105
260
|
|
|
106
|
-
###
|
|
261
|
+
### 5. Configure MCP Servers
|
|
107
262
|
|
|
108
263
|
Claude Code and Codex can be configured with MCP servers by passing
|
|
109
264
|
`HttpMcpServer` and `StdioMcpServer` objects at construction time.
|
|
@@ -154,14 +309,13 @@ class MyAgent(BaseCodingAgent):
|
|
|
154
309
|
self,
|
|
155
310
|
model: str | None = None,
|
|
156
311
|
region: str | None = None,
|
|
157
|
-
recorder=None,
|
|
158
312
|
event_handler=None,
|
|
313
|
+
event_handlers=None,
|
|
159
314
|
mcp_servers=None,
|
|
160
315
|
sandbox=False,
|
|
161
316
|
):
|
|
162
317
|
self.model = model
|
|
163
318
|
self.region = region
|
|
164
|
-
self.recorder = recorder
|
|
165
319
|
self.event_handler = event_handler
|
|
166
320
|
|
|
167
321
|
def generate(self, prompt: str, cwd=None, timeout=300, silent=False) -> str:
|
|
@@ -181,7 +335,7 @@ Notes:
|
|
|
181
335
|
- Registration is import-driven. Your provider is available only after the module defining it has been imported in the current Python process.
|
|
182
336
|
- `list_providers()` returns canonical provider names only. Aliases resolve via `get_provider_class(...)` and `CodingAgent(provider=...)`.
|
|
183
337
|
- `register_provider(...)` rejects invalid names, abstract classes, and accidental name collisions unless you pass `overwrite=True`.
|
|
184
|
-
- If you want `CodingAgent(...)` to instantiate your provider, its constructor should accept the shared kwargs `model`, `
|
|
338
|
+
- If you want `CodingAgent(...)` to instantiate your provider, its constructor should accept the shared kwargs `model`, `event_handler`, `event_handlers`, `mcp_servers`, `sandbox`, and `executor` as needed.
|
|
185
339
|
- If your provider needs extra constructor arguments beyond the shared portable set, pass them via `backend_kwargs={...}` when constructing `CodingAgent(...)`.
|
|
186
340
|
|
|
187
341
|
## Development
|
|
@@ -11,10 +11,10 @@ prompting, session resumption, event parsing, or MCP configuration.
|
|
|
11
11
|
- a shared CLI agent abstraction with a provider registry
|
|
12
12
|
- adapters for Claude Code, Codex, Gemini, and Opencode
|
|
13
13
|
- stateful chat sessions that automatically resume provider-native threads
|
|
14
|
+
- injectable command executors for custom process launching or sandboxing
|
|
14
15
|
- MCP server config models for providers that support MCP
|
|
15
16
|
- sandbox settings helpers for Claude Code
|
|
16
17
|
- a lightweight LiteLLM client and subagent helper
|
|
17
|
-
- trajectory/usage helpers used by higher-level runtimes
|
|
18
18
|
|
|
19
19
|
## Install
|
|
20
20
|
|
|
@@ -70,7 +70,162 @@ reply = agent.generate("Write a short summary of this codebase.", cwd=".")
|
|
|
70
70
|
print(reply)
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
### 2.
|
|
73
|
+
### 2. Handle Agent Events
|
|
74
|
+
|
|
75
|
+
By default, `agentshim` prints provider events to the terminal through a
|
|
76
|
+
`ConsoleEventHandler`. That default is used only when you do not provide your
|
|
77
|
+
own event handler and `silent=False`.
|
|
78
|
+
|
|
79
|
+
If you pass `event_handler=...`, you take ownership of event handling. The
|
|
80
|
+
built-in console printer is not added implicitly, which avoids surprising
|
|
81
|
+
duplicate output.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from agentshim import CodingAgent
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class MyHandler:
|
|
88
|
+
def on_thinking(self, text: str) -> None:
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
def on_tool_call(self, tool: str, args=None) -> None:
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
def on_tool_result(
|
|
95
|
+
self,
|
|
96
|
+
tool: str,
|
|
97
|
+
stdout: str = "",
|
|
98
|
+
stderr: str = "",
|
|
99
|
+
exit_code: int | None = None,
|
|
100
|
+
duration: float | None = None,
|
|
101
|
+
) -> None:
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
def on_usage(self, usage: dict) -> None:
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
agent = CodingAgent(provider="claude", event_handler=MyHandler())
|
|
109
|
+
agent.generate("Inspect this repository.")
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
To keep the default console output and add your own handler, compose them
|
|
113
|
+
explicitly:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from agentshim import CodingAgent, ConsoleEventHandler
|
|
117
|
+
|
|
118
|
+
agent = CodingAgent(
|
|
119
|
+
provider="claude",
|
|
120
|
+
event_handlers=[
|
|
121
|
+
ConsoleEventHandler(),
|
|
122
|
+
MyHandler(),
|
|
123
|
+
],
|
|
124
|
+
)
|
|
125
|
+
agent.generate("Inspect this repository.")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
You can also build the composition yourself:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from agentshim import CompositeEventHandler, ConsoleEventHandler
|
|
132
|
+
|
|
133
|
+
handler = CompositeEventHandler([ConsoleEventHandler(), MyHandler()])
|
|
134
|
+
agent = CodingAgent(provider="codex", event_handler=handler)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Use `silent=True` to suppress the default console handler when you have not
|
|
138
|
+
provided any handler:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
agent = CodingAgent(provider="claude")
|
|
142
|
+
reply = agent.generate("Return only the answer.", silent=True)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 3. Run Commands Through a Custom Executor
|
|
146
|
+
|
|
147
|
+
Provider classes accept an optional `executor=`. The executor controls binary
|
|
148
|
+
lookup, CLI validation, and streaming process execution while `agentshim` keeps
|
|
149
|
+
owning provider command construction, stdout/stderr parsing, session state, and
|
|
150
|
+
event emission.
|
|
151
|
+
|
|
152
|
+
This is useful when a caller needs to run the CLI somewhere other than the
|
|
153
|
+
current host process, for example through an existing container, remote shell,
|
|
154
|
+
or custom sandbox.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from agentshim import (
|
|
158
|
+
CodexCodingAgent,
|
|
159
|
+
CommandHandle,
|
|
160
|
+
CommandRequest,
|
|
161
|
+
CommandResult,
|
|
162
|
+
CommandStreamSink,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class MyCommandHandle:
|
|
167
|
+
def terminate(self) -> None:
|
|
168
|
+
...
|
|
169
|
+
|
|
170
|
+
def kill(self) -> None:
|
|
171
|
+
...
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class MyExecutor:
|
|
175
|
+
def find_binary(self, binary_name: str, env: dict[str, str]) -> str:
|
|
176
|
+
# This value becomes request.argv[0]. Container or remote executors can
|
|
177
|
+
# return the binary name if lookup happens in the target runtime.
|
|
178
|
+
return binary_name
|
|
179
|
+
|
|
180
|
+
def check_binary(self, binary_path: str, env: dict[str, str], *, timeout: int) -> None:
|
|
181
|
+
# Raise RuntimeError if the target CLI is unavailable. No-op is fine
|
|
182
|
+
# when validation is not cheap or is handled by the runtime.
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
def run(self, request: CommandRequest, sink: CommandStreamSink) -> CommandResult:
|
|
186
|
+
handle = MyCommandHandle()
|
|
187
|
+
sink.started(handle)
|
|
188
|
+
|
|
189
|
+
stdout = ""
|
|
190
|
+
stderr = ""
|
|
191
|
+
|
|
192
|
+
# Run request.argv in your target runtime, with request.stdin,
|
|
193
|
+
# request.cwd, request.env, and request.timeout. Stream each complete
|
|
194
|
+
# line as it arrives, preserving trailing newlines when present.
|
|
195
|
+
line = "streamed output\n"
|
|
196
|
+
stdout += line
|
|
197
|
+
sink.stdout(line)
|
|
198
|
+
|
|
199
|
+
return CommandResult(returncode=0, stdout=stdout, stderr=stderr)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
agent = CodexCodingAgent(executor=MyExecutor())
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The default `HostCommandExecutor` preserves the normal local `subprocess`
|
|
206
|
+
behavior. `CodingAgent(provider=..., executor=...)` forwards the same executor
|
|
207
|
+
to the selected provider.
|
|
208
|
+
|
|
209
|
+
Executor contract:
|
|
210
|
+
|
|
211
|
+
- `CommandRequest.argv` is the complete provider CLI command. `argv[0]` is the
|
|
212
|
+
value returned by `find_binary`.
|
|
213
|
+
- `CommandRequest.stdin` is the prompt text to write to the command's standard
|
|
214
|
+
input, then stdin should be closed.
|
|
215
|
+
- `CommandRequest.cwd`, `env`, and `timeout` should be honored by the executor.
|
|
216
|
+
- Call `sink.started(handle)` once after the command starts. The handle only
|
|
217
|
+
needs `terminate()` and `kill()`.
|
|
218
|
+
- Call `sink.stdout(line)` and `sink.stderr(line)` as output is produced. Lines
|
|
219
|
+
should include trailing newlines when the underlying stream provided them.
|
|
220
|
+
- Return `CommandResult(returncode, stdout, stderr)` after the command exits.
|
|
221
|
+
The returned text should match what was streamed through the sink.
|
|
222
|
+
|
|
223
|
+
If you were using the pre-0.5 executor preview, replace
|
|
224
|
+
`run_streaming(cmd, ..., on_stdout, on_stderr, on_process_started)` with
|
|
225
|
+
`run(request, sink)`. The parser/event APIs remain internal; custom executors
|
|
226
|
+
only provide a stable command runtime.
|
|
227
|
+
|
|
228
|
+
### 4. Instantiate a Specific Provider Directly
|
|
74
229
|
|
|
75
230
|
If you already know which backend you want, construct the provider class
|
|
76
231
|
yourself.
|
|
@@ -92,7 +247,7 @@ The bundled provider classes are:
|
|
|
92
247
|
- `GeminiCodingAgent`
|
|
93
248
|
- `OpencodeCodingAgent`
|
|
94
249
|
|
|
95
|
-
###
|
|
250
|
+
### 5. Configure MCP Servers
|
|
96
251
|
|
|
97
252
|
Claude Code and Codex can be configured with MCP servers by passing
|
|
98
253
|
`HttpMcpServer` and `StdioMcpServer` objects at construction time.
|
|
@@ -143,14 +298,13 @@ class MyAgent(BaseCodingAgent):
|
|
|
143
298
|
self,
|
|
144
299
|
model: str | None = None,
|
|
145
300
|
region: str | None = None,
|
|
146
|
-
recorder=None,
|
|
147
301
|
event_handler=None,
|
|
302
|
+
event_handlers=None,
|
|
148
303
|
mcp_servers=None,
|
|
149
304
|
sandbox=False,
|
|
150
305
|
):
|
|
151
306
|
self.model = model
|
|
152
307
|
self.region = region
|
|
153
|
-
self.recorder = recorder
|
|
154
308
|
self.event_handler = event_handler
|
|
155
309
|
|
|
156
310
|
def generate(self, prompt: str, cwd=None, timeout=300, silent=False) -> str:
|
|
@@ -170,7 +324,7 @@ Notes:
|
|
|
170
324
|
- Registration is import-driven. Your provider is available only after the module defining it has been imported in the current Python process.
|
|
171
325
|
- `list_providers()` returns canonical provider names only. Aliases resolve via `get_provider_class(...)` and `CodingAgent(provider=...)`.
|
|
172
326
|
- `register_provider(...)` rejects invalid names, abstract classes, and accidental name collisions unless you pass `overwrite=True`.
|
|
173
|
-
- If you want `CodingAgent(...)` to instantiate your provider, its constructor should accept the shared kwargs `model`, `
|
|
327
|
+
- If you want `CodingAgent(...)` to instantiate your provider, its constructor should accept the shared kwargs `model`, `event_handler`, `event_handlers`, `mcp_servers`, `sandbox`, and `executor` as needed.
|
|
174
328
|
- If your provider needs extra constructor arguments beyond the shared portable set, pass them via `backend_kwargs={...}` when constructing `CodingAgent(...)`.
|
|
175
329
|
|
|
176
330
|
## Development
|
|
@@ -2,6 +2,16 @@ from .base import BaseAgentSession, BaseCodingAgent, CodingAgent, get_provider_c
|
|
|
2
2
|
from .claude import ClaudeCodeCodingAgent
|
|
3
3
|
from .copilot import CopilotCodingAgent
|
|
4
4
|
from .codex import CodexCodingAgent
|
|
5
|
+
from .events import CompositeEventHandler, ConsoleEventHandler, NullEventHandler
|
|
6
|
+
from .executor import (
|
|
7
|
+
CallbackCommandStreamSink,
|
|
8
|
+
CommandExecutor,
|
|
9
|
+
CommandHandle,
|
|
10
|
+
CommandRequest,
|
|
11
|
+
CommandResult,
|
|
12
|
+
CommandStreamSink,
|
|
13
|
+
HostCommandExecutor,
|
|
14
|
+
)
|
|
5
15
|
from .gemini import GeminiCodingAgent
|
|
6
16
|
from .mcp_config import HttpMcpServer, McpServerConfig, StdioMcpServer
|
|
7
17
|
from .opencode import OpencodeCodingAgent
|
|
@@ -14,6 +24,16 @@ __all__ = [
|
|
|
14
24
|
"get_provider_class",
|
|
15
25
|
"list_providers",
|
|
16
26
|
"register_provider",
|
|
27
|
+
"CompositeEventHandler",
|
|
28
|
+
"ConsoleEventHandler",
|
|
29
|
+
"NullEventHandler",
|
|
30
|
+
"CallbackCommandStreamSink",
|
|
31
|
+
"CommandExecutor",
|
|
32
|
+
"CommandHandle",
|
|
33
|
+
"CommandRequest",
|
|
34
|
+
"CommandResult",
|
|
35
|
+
"CommandStreamSink",
|
|
36
|
+
"HostCommandExecutor",
|
|
17
37
|
"CopilotCodingAgent",
|
|
18
38
|
"CodexCodingAgent",
|
|
19
39
|
"GeminiCodingAgent",
|
|
@@ -7,9 +7,9 @@ from collections.abc import Callable, Sequence
|
|
|
7
7
|
from typing import Any, TypeVar
|
|
8
8
|
|
|
9
9
|
from agentshim.events import AgentEventHandler
|
|
10
|
+
from agentshim.executor import CommandExecutor, CommandHandle
|
|
10
11
|
from agentshim.mcp_config import McpServerConfig
|
|
11
12
|
from agentshim.sandbox import SandboxConfig
|
|
12
|
-
from agentshim.trajectory import NullTrajectoryRecorder, TrajectoryRecorderProtocol
|
|
13
13
|
|
|
14
14
|
_T = TypeVar("_T")
|
|
15
15
|
_READABLE_NAME_BOUNDARY = re.compile(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
|
|
@@ -28,7 +28,6 @@ def _readable_name_from_class_name(class_name: str) -> str:
|
|
|
28
28
|
class BaseCodingAgent(ABC):
|
|
29
29
|
"""Abstract base class for coding agents."""
|
|
30
30
|
|
|
31
|
-
recorder: TrajectoryRecorderProtocol = NullTrajectoryRecorder()
|
|
32
31
|
event_handler: Any | None = None
|
|
33
32
|
|
|
34
33
|
@property
|
|
@@ -85,7 +84,7 @@ class BaseAgentSession(ABC):
|
|
|
85
84
|
cwd: str | None = None,
|
|
86
85
|
timeout: int | None = None,
|
|
87
86
|
silent: bool | None = None,
|
|
88
|
-
on_process_started: Callable[[
|
|
87
|
+
on_process_started: Callable[[CommandHandle], None] | None = None,
|
|
89
88
|
) -> str:
|
|
90
89
|
"""Send ``prompt`` within an existing chat session."""
|
|
91
90
|
|
|
@@ -113,9 +112,7 @@ class ProviderRegistry:
|
|
|
113
112
|
if not normalized:
|
|
114
113
|
raise ValueError("provider name must not be empty")
|
|
115
114
|
if not _PROVIDER_NAME_PATTERN.fullmatch(normalized):
|
|
116
|
-
raise ValueError(
|
|
117
|
-
f"invalid provider name '{name}'; use lowercase letters, digits, hyphens, or underscores"
|
|
118
|
-
)
|
|
115
|
+
raise ValueError(f"invalid provider name '{name}'; use lowercase letters, digits, hyphens, or underscores")
|
|
119
116
|
return normalized
|
|
120
117
|
|
|
121
118
|
def _normalize_names(self, canonical_name: str, aliases: tuple[str, ...]) -> tuple[str, tuple[str, ...]]:
|
|
@@ -147,9 +144,7 @@ class ProviderRegistry:
|
|
|
147
144
|
all_names = (canonical, *normalized_aliases)
|
|
148
145
|
|
|
149
146
|
collisions = [
|
|
150
|
-
name
|
|
151
|
-
for name in all_names
|
|
152
|
-
if name in self._providers and self._providers[name] is not provider_cls
|
|
147
|
+
name for name in all_names if name in self._providers and self._providers[name] is not provider_cls
|
|
153
148
|
]
|
|
154
149
|
if collisions and not overwrite:
|
|
155
150
|
raise ValueError(
|
|
@@ -172,18 +167,14 @@ class ProviderRegistry:
|
|
|
172
167
|
normalized = self._normalize_name(name)
|
|
173
168
|
provider_cls = self._providers.get(normalized)
|
|
174
169
|
if provider_cls is None:
|
|
175
|
-
raise ValueError(
|
|
176
|
-
f"Unknown coding agent provider '{name}'. Available providers: {self.list_providers()}"
|
|
177
|
-
)
|
|
170
|
+
raise ValueError(f"Unknown coding agent provider '{name}'. Available providers: {self.list_providers()}")
|
|
178
171
|
return provider_cls
|
|
179
172
|
|
|
180
173
|
def get_canonical_name(self, name: str) -> str:
|
|
181
174
|
normalized = self._normalize_name(name)
|
|
182
175
|
canonical = self._canonical_names.get(normalized)
|
|
183
176
|
if canonical is None:
|
|
184
|
-
raise ValueError(
|
|
185
|
-
f"Unknown coding agent provider '{name}'. Available providers: {self.list_providers()}"
|
|
186
|
-
)
|
|
177
|
+
raise ValueError(f"Unknown coding agent provider '{name}'. Available providers: {self.list_providers()}")
|
|
187
178
|
return canonical
|
|
188
179
|
|
|
189
180
|
|
|
@@ -195,7 +186,7 @@ def register_provider(
|
|
|
195
186
|
*extra_names: str,
|
|
196
187
|
aliases: tuple[str, ...] = (),
|
|
197
188
|
overwrite: bool = False,
|
|
198
|
-
) -> Callable[[type[_T]], _T]:
|
|
189
|
+
) -> Callable[[type[_T]], type[_T]]:
|
|
199
190
|
"""Decorator to register a coding agent provider.
|
|
200
191
|
|
|
201
192
|
Registration is import-driven: the decorated class becomes available only
|
|
@@ -211,7 +202,7 @@ def register_provider(
|
|
|
211
202
|
all_aliases = (*extra_names, *aliases)
|
|
212
203
|
_PROVIDER_REGISTRY._normalize_names(canonical_name, all_aliases)
|
|
213
204
|
|
|
214
|
-
def decorator(cls: type[_T]) -> _T:
|
|
205
|
+
def decorator(cls: type[_T]) -> type[_T]:
|
|
215
206
|
return _PROVIDER_REGISTRY.register(
|
|
216
207
|
cls,
|
|
217
208
|
canonical_name=canonical_name,
|
|
@@ -243,10 +234,11 @@ class CodingAgent(BaseCodingAgent):
|
|
|
243
234
|
self,
|
|
244
235
|
provider: str,
|
|
245
236
|
model: str | None = None,
|
|
246
|
-
recorder: TrajectoryRecorderProtocol | None = None,
|
|
247
237
|
event_handler: AgentEventHandler | None = None,
|
|
238
|
+
event_handlers: Sequence[AgentEventHandler] | None = None,
|
|
248
239
|
mcp_servers: Sequence[McpServerConfig] | None = None,
|
|
249
240
|
sandbox: bool | SandboxConfig | None = False,
|
|
241
|
+
executor: CommandExecutor | None = None,
|
|
250
242
|
backend_kwargs: dict[str, Any] | None = None,
|
|
251
243
|
) -> None:
|
|
252
244
|
requested_provider = _PROVIDER_REGISTRY.get_canonical_name(provider)
|
|
@@ -256,21 +248,22 @@ class CodingAgent(BaseCodingAgent):
|
|
|
256
248
|
portable_kwargs: dict[str, Any] = {}
|
|
257
249
|
if model is not None:
|
|
258
250
|
portable_kwargs["model"] = model
|
|
259
|
-
if recorder is not None:
|
|
260
|
-
portable_kwargs["recorder"] = recorder
|
|
261
251
|
if event_handler is not None:
|
|
262
252
|
portable_kwargs["event_handler"] = event_handler
|
|
253
|
+
if event_handlers is not None:
|
|
254
|
+
portable_kwargs["event_handlers"] = list(event_handlers)
|
|
263
255
|
if mcp_servers is not None:
|
|
264
256
|
portable_kwargs["mcp_servers"] = list(mcp_servers)
|
|
265
257
|
if sandbox is not None and sandbox is not False:
|
|
266
258
|
portable_kwargs["sandbox"] = sandbox
|
|
259
|
+
if executor is not None:
|
|
260
|
+
portable_kwargs["executor"] = executor
|
|
267
261
|
|
|
268
262
|
advanced_kwargs = dict(backend_kwargs or {})
|
|
269
263
|
overlapping_keys = sorted(portable_kwargs.keys() & advanced_kwargs.keys())
|
|
270
264
|
if overlapping_keys:
|
|
271
265
|
raise ValueError(
|
|
272
|
-
"backend_kwargs must not override portable CodingAgent arguments: "
|
|
273
|
-
+ ", ".join(overlapping_keys)
|
|
266
|
+
"backend_kwargs must not override portable CodingAgent arguments: " + ", ".join(overlapping_keys)
|
|
274
267
|
)
|
|
275
268
|
|
|
276
269
|
self._backend: BaseCodingAgent = provider_cls(**portable_kwargs, **advanced_kwargs)
|
|
@@ -296,14 +289,6 @@ class CodingAgent(BaseCodingAgent):
|
|
|
296
289
|
def model(self, value: Any) -> None:
|
|
297
290
|
self._backend.model = value # type: ignore[attr-defined]
|
|
298
291
|
|
|
299
|
-
@property
|
|
300
|
-
def recorder(self) -> TrajectoryRecorderProtocol:
|
|
301
|
-
return self._backend.recorder
|
|
302
|
-
|
|
303
|
-
@recorder.setter
|
|
304
|
-
def recorder(self, value: TrajectoryRecorderProtocol) -> None:
|
|
305
|
-
self._backend.recorder = value
|
|
306
|
-
|
|
307
292
|
@property
|
|
308
293
|
def event_handler(self) -> Any | None:
|
|
309
294
|
return getattr(self._backend, "event_handler", None)
|