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.
Files changed (69) hide show
  1. agentshim-0.5.0/.github/workflows/ci.yml +41 -0
  2. {agentshim-0.3.1 → agentshim-0.5.0}/PKG-INFO +161 -7
  3. {agentshim-0.3.1 → agentshim-0.5.0}/README.md +160 -6
  4. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/__init__.py +20 -0
  5. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/base.py +15 -30
  6. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/claude/agent.py +11 -40
  7. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/claude/events.py +1 -34
  8. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/cli_agent.py +62 -176
  9. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/codex/agent.py +13 -43
  10. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/codex/events.py +1 -33
  11. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/copilot/agent.py +23 -50
  12. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/copilot/events.py +1 -47
  13. agentshim-0.5.0/agentshim/events.py +263 -0
  14. agentshim-0.5.0/agentshim/executor.py +319 -0
  15. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/gemini/agent.py +14 -87
  16. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/gemini/events.py +1 -26
  17. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/llm_client.py +4 -15
  18. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/opencode/agent.py +23 -39
  19. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/opencode/events.py +2 -35
  20. {agentshim-0.3.1 → agentshim-0.5.0}/pyproject.toml +1 -1
  21. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/cli_agent/test_agent_cli_cleanup.py +0 -1
  22. agentshim-0.5.0/tests/unit/cli_agent/test_command_executor.py +245 -0
  23. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/llm/test_claude_stream.py +25 -27
  24. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_claude.py +5 -9
  25. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_copilot.py +41 -11
  26. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_copilot_fixtures.py +7 -7
  27. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_event_parsing.py +0 -48
  28. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_resume.py +0 -6
  29. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_coding_agent_facade.py +19 -7
  30. agentshim-0.5.0/tests/unit/test_event_handlers.py +83 -0
  31. {agentshim-0.3.1 → agentshim-0.5.0}/uv.lock +1 -1
  32. agentshim-0.3.1/.github/workflows/ci.yml +0 -18
  33. agentshim-0.3.1/agentshim/events.py +0 -28
  34. agentshim-0.3.1/agentshim/trajectory.py +0 -168
  35. agentshim-0.3.1/tests/unit/test_coding_agent_recorder_default.py +0 -12
  36. {agentshim-0.3.1 → agentshim-0.5.0}/.github/workflows/publish.yml +0 -0
  37. {agentshim-0.3.1 → agentshim-0.5.0}/.gitignore +0 -0
  38. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/claude/__init__.py +0 -0
  39. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/claude/hooks/__init__.py +0 -0
  40. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/claude/hooks/confine_reads.py +0 -0
  41. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/claude_events.py +0 -0
  42. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/codex/__init__.py +0 -0
  43. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/codex_events.py +0 -0
  44. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/copilot/__init__.py +0 -0
  45. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/copilot_events.py +0 -0
  46. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/gemini/__init__.py +0 -0
  47. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/gemini_events.py +0 -0
  48. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/mcp_config.py +0 -0
  49. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/opencode/__init__.py +0 -0
  50. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/opencode_events.py +0 -0
  51. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/py.typed +0 -0
  52. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/sandbox.py +0 -0
  53. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/subagent.py +0 -0
  54. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/usage.py +0 -0
  55. {agentshim-0.3.1 → agentshim-0.5.0}/agentshim/utils.py +0 -0
  56. {agentshim-0.3.1 → agentshim-0.5.0}/tests/fixtures/copilot/session_turn_1.jsonl +0 -0
  57. {agentshim-0.3.1 → agentshim-0.5.0}/tests/fixtures/copilot/session_turn_2_resumed.jsonl +0 -0
  58. {agentshim-0.3.1 → agentshim-0.5.0}/tests/fixtures/copilot/streaming_dedup.jsonl +0 -0
  59. {agentshim-0.3.1 → agentshim-0.5.0}/tests/fixtures/copilot/tool_and_usage.jsonl +0 -0
  60. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/cli_agent/test_check_cli.py +0 -0
  61. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/cli_agent/test_cli_prompt_passing.py +0 -0
  62. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/llm/conftest.py +0 -0
  63. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/llm/test_gemini_fixture.py +0 -0
  64. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/llm/test_gemini_stream.py +0 -0
  65. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_codex.py +0 -0
  66. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_mcp_unsupported.py +0 -0
  67. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_agent_cli_sandbox.py +0 -0
  68. {agentshim-0.3.1 → agentshim-0.5.0}/tests/unit/test_cli_agent_usage.py +0 -0
  69. {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.1
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. Instantiate a Specific Provider Directly
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
- ### 3. Configure MCP Servers
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`, `recorder`, `event_handler`, `mcp_servers`, and `sandbox` as needed.
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. Instantiate a Specific Provider Directly
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
- ### 3. Configure MCP Servers
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`, `recorder`, `event_handler`, `mcp_servers`, and `sandbox` as needed.
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[[Any], None] | None = None,
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)