agentshim 0.3.0__tar.gz → 0.4.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.3.0 → agentshim-0.4.0}/PKG-INFO +77 -7
- {agentshim-0.3.0 → agentshim-0.4.0}/README.md +76 -6
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/__init__.py +4 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/base.py +8 -27
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/claude/agent.py +12 -37
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/claude/events.py +12 -36
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/cli_agent.py +25 -54
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/codex/agent.py +75 -49
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/codex/events.py +37 -47
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/copilot/agent.py +18 -47
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/copilot/events.py +1 -47
- agentshim-0.4.0/agentshim/events.py +263 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/gemini/agent.py +8 -84
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/gemini/events.py +1 -26
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/llm_client.py +4 -15
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/opencode/agent.py +17 -36
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/opencode/events.py +2 -35
- {agentshim-0.3.0 → agentshim-0.4.0}/pyproject.toml +1 -1
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/cli_agent/test_agent_cli_cleanup.py +0 -1
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/llm/test_claude_stream.py +25 -27
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/test_agent_cli_claude.py +67 -5
- agentshim-0.4.0/tests/unit/test_agent_cli_codex.py +243 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/test_agent_cli_copilot.py +41 -11
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/test_agent_cli_copilot_fixtures.py +7 -7
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/test_agent_cli_event_parsing.py +62 -52
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/test_agent_cli_resume.py +0 -6
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/test_cli_agent_usage.py +37 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/test_coding_agent_facade.py +12 -7
- agentshim-0.4.0/tests/unit/test_event_handlers.py +83 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/uv.lock +1 -1
- agentshim-0.3.0/agentshim/events.py +0 -24
- agentshim-0.3.0/agentshim/trajectory.py +0 -168
- agentshim-0.3.0/tests/unit/test_agent_cli_codex.py +0 -69
- agentshim-0.3.0/tests/unit/test_coding_agent_recorder_default.py +0 -12
- {agentshim-0.3.0 → agentshim-0.4.0}/.github/workflows/ci.yml +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/.github/workflows/publish.yml +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/.gitignore +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/claude/__init__.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/claude/hooks/__init__.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/claude_events.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/codex/__init__.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/codex_events.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/copilot/__init__.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/copilot_events.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/gemini/__init__.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/gemini_events.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/mcp_config.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/opencode/__init__.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/opencode_events.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/py.typed +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/sandbox.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/subagent.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/usage.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/agentshim/utils.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/fixtures/copilot/session_turn_1.jsonl +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/fixtures/copilot/session_turn_2_resumed.jsonl +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/fixtures/copilot/streaming_dedup.jsonl +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/fixtures/copilot/tool_and_usage.jsonl +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/cli_agent/test_check_cli.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/cli_agent/test_cli_prompt_passing.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/llm/conftest.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/llm/test_gemini_fixture.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/llm/test_gemini_stream.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/test_agent_cli_mcp_unsupported.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/test_agent_cli_sandbox.py +0 -0
- {agentshim-0.3.0 → agentshim-0.4.0}/tests/unit/test_mcp_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentshim
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Provider-agnostic coding-agent CLI shims
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: litellm>=1.0.0
|
|
@@ -25,7 +25,6 @@ prompting, session resumption, event parsing, or MCP configuration.
|
|
|
25
25
|
- MCP server config models for providers that support MCP
|
|
26
26
|
- sandbox settings helpers for Claude Code
|
|
27
27
|
- a lightweight LiteLLM client and subagent helper
|
|
28
|
-
- trajectory/usage helpers used by higher-level runtimes
|
|
29
28
|
|
|
30
29
|
## Install
|
|
31
30
|
|
|
@@ -81,7 +80,79 @@ reply = agent.generate("Write a short summary of this codebase.", cwd=".")
|
|
|
81
80
|
print(reply)
|
|
82
81
|
```
|
|
83
82
|
|
|
84
|
-
### 2.
|
|
83
|
+
### 2. Handle Agent Events
|
|
84
|
+
|
|
85
|
+
By default, `agentshim` prints provider events to the terminal through a
|
|
86
|
+
`ConsoleEventHandler`. That default is used only when you do not provide your
|
|
87
|
+
own event handler and `silent=False`.
|
|
88
|
+
|
|
89
|
+
If you pass `event_handler=...`, you take ownership of event handling. The
|
|
90
|
+
built-in console printer is not added implicitly, which avoids surprising
|
|
91
|
+
duplicate output.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from agentshim import CodingAgent
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class MyHandler:
|
|
98
|
+
def on_thinking(self, text: str) -> None:
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
def on_tool_call(self, tool: str, args=None) -> None:
|
|
102
|
+
...
|
|
103
|
+
|
|
104
|
+
def on_tool_result(
|
|
105
|
+
self,
|
|
106
|
+
tool: str,
|
|
107
|
+
stdout: str = "",
|
|
108
|
+
stderr: str = "",
|
|
109
|
+
exit_code: int | None = None,
|
|
110
|
+
duration: float | None = None,
|
|
111
|
+
) -> None:
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
def on_usage(self, usage: dict) -> None:
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
agent = CodingAgent(provider="claude", event_handler=MyHandler())
|
|
119
|
+
agent.generate("Inspect this repository.")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
To keep the default console output and add your own handler, compose them
|
|
123
|
+
explicitly:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from agentshim import CodingAgent, ConsoleEventHandler
|
|
127
|
+
|
|
128
|
+
agent = CodingAgent(
|
|
129
|
+
provider="claude",
|
|
130
|
+
event_handlers=[
|
|
131
|
+
ConsoleEventHandler(),
|
|
132
|
+
MyHandler(),
|
|
133
|
+
],
|
|
134
|
+
)
|
|
135
|
+
agent.generate("Inspect this repository.")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
You can also build the composition yourself:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from agentshim import CompositeEventHandler, ConsoleEventHandler
|
|
142
|
+
|
|
143
|
+
handler = CompositeEventHandler([ConsoleEventHandler(), MyHandler()])
|
|
144
|
+
agent = CodingAgent(provider="codex", event_handler=handler)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Use `silent=True` to suppress the default console handler when you have not
|
|
148
|
+
provided any handler:
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
agent = CodingAgent(provider="claude")
|
|
152
|
+
reply = agent.generate("Return only the answer.", silent=True)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 3. Instantiate a Specific Provider Directly
|
|
85
156
|
|
|
86
157
|
If you already know which backend you want, construct the provider class
|
|
87
158
|
yourself.
|
|
@@ -103,7 +174,7 @@ The bundled provider classes are:
|
|
|
103
174
|
- `GeminiCodingAgent`
|
|
104
175
|
- `OpencodeCodingAgent`
|
|
105
176
|
|
|
106
|
-
###
|
|
177
|
+
### 4. Configure MCP Servers
|
|
107
178
|
|
|
108
179
|
Claude Code and Codex can be configured with MCP servers by passing
|
|
109
180
|
`HttpMcpServer` and `StdioMcpServer` objects at construction time.
|
|
@@ -154,14 +225,13 @@ class MyAgent(BaseCodingAgent):
|
|
|
154
225
|
self,
|
|
155
226
|
model: str | None = None,
|
|
156
227
|
region: str | None = None,
|
|
157
|
-
recorder=None,
|
|
158
228
|
event_handler=None,
|
|
229
|
+
event_handlers=None,
|
|
159
230
|
mcp_servers=None,
|
|
160
231
|
sandbox=False,
|
|
161
232
|
):
|
|
162
233
|
self.model = model
|
|
163
234
|
self.region = region
|
|
164
|
-
self.recorder = recorder
|
|
165
235
|
self.event_handler = event_handler
|
|
166
236
|
|
|
167
237
|
def generate(self, prompt: str, cwd=None, timeout=300, silent=False) -> str:
|
|
@@ -181,7 +251,7 @@ Notes:
|
|
|
181
251
|
- Registration is import-driven. Your provider is available only after the module defining it has been imported in the current Python process.
|
|
182
252
|
- `list_providers()` returns canonical provider names only. Aliases resolve via `get_provider_class(...)` and `CodingAgent(provider=...)`.
|
|
183
253
|
- `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`, `
|
|
254
|
+
- If you want `CodingAgent(...)` to instantiate your provider, its constructor should accept the shared kwargs `model`, `event_handler`, `event_handlers`, `mcp_servers`, and `sandbox` as needed.
|
|
185
255
|
- If your provider needs extra constructor arguments beyond the shared portable set, pass them via `backend_kwargs={...}` when constructing `CodingAgent(...)`.
|
|
186
256
|
|
|
187
257
|
## Development
|
|
@@ -14,7 +14,6 @@ prompting, session resumption, event parsing, or MCP configuration.
|
|
|
14
14
|
- MCP server config models for providers that support MCP
|
|
15
15
|
- sandbox settings helpers for Claude Code
|
|
16
16
|
- a lightweight LiteLLM client and subagent helper
|
|
17
|
-
- trajectory/usage helpers used by higher-level runtimes
|
|
18
17
|
|
|
19
18
|
## Install
|
|
20
19
|
|
|
@@ -70,7 +69,79 @@ reply = agent.generate("Write a short summary of this codebase.", cwd=".")
|
|
|
70
69
|
print(reply)
|
|
71
70
|
```
|
|
72
71
|
|
|
73
|
-
### 2.
|
|
72
|
+
### 2. Handle Agent Events
|
|
73
|
+
|
|
74
|
+
By default, `agentshim` prints provider events to the terminal through a
|
|
75
|
+
`ConsoleEventHandler`. That default is used only when you do not provide your
|
|
76
|
+
own event handler and `silent=False`.
|
|
77
|
+
|
|
78
|
+
If you pass `event_handler=...`, you take ownership of event handling. The
|
|
79
|
+
built-in console printer is not added implicitly, which avoids surprising
|
|
80
|
+
duplicate output.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from agentshim import CodingAgent
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class MyHandler:
|
|
87
|
+
def on_thinking(self, text: str) -> None:
|
|
88
|
+
...
|
|
89
|
+
|
|
90
|
+
def on_tool_call(self, tool: str, args=None) -> None:
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
def on_tool_result(
|
|
94
|
+
self,
|
|
95
|
+
tool: str,
|
|
96
|
+
stdout: str = "",
|
|
97
|
+
stderr: str = "",
|
|
98
|
+
exit_code: int | None = None,
|
|
99
|
+
duration: float | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
def on_usage(self, usage: dict) -> None:
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
agent = CodingAgent(provider="claude", event_handler=MyHandler())
|
|
108
|
+
agent.generate("Inspect this repository.")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
To keep the default console output and add your own handler, compose them
|
|
112
|
+
explicitly:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from agentshim import CodingAgent, ConsoleEventHandler
|
|
116
|
+
|
|
117
|
+
agent = CodingAgent(
|
|
118
|
+
provider="claude",
|
|
119
|
+
event_handlers=[
|
|
120
|
+
ConsoleEventHandler(),
|
|
121
|
+
MyHandler(),
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
agent.generate("Inspect this repository.")
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
You can also build the composition yourself:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from agentshim import CompositeEventHandler, ConsoleEventHandler
|
|
131
|
+
|
|
132
|
+
handler = CompositeEventHandler([ConsoleEventHandler(), MyHandler()])
|
|
133
|
+
agent = CodingAgent(provider="codex", event_handler=handler)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Use `silent=True` to suppress the default console handler when you have not
|
|
137
|
+
provided any handler:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
agent = CodingAgent(provider="claude")
|
|
141
|
+
reply = agent.generate("Return only the answer.", silent=True)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 3. Instantiate a Specific Provider Directly
|
|
74
145
|
|
|
75
146
|
If you already know which backend you want, construct the provider class
|
|
76
147
|
yourself.
|
|
@@ -92,7 +163,7 @@ The bundled provider classes are:
|
|
|
92
163
|
- `GeminiCodingAgent`
|
|
93
164
|
- `OpencodeCodingAgent`
|
|
94
165
|
|
|
95
|
-
###
|
|
166
|
+
### 4. Configure MCP Servers
|
|
96
167
|
|
|
97
168
|
Claude Code and Codex can be configured with MCP servers by passing
|
|
98
169
|
`HttpMcpServer` and `StdioMcpServer` objects at construction time.
|
|
@@ -143,14 +214,13 @@ class MyAgent(BaseCodingAgent):
|
|
|
143
214
|
self,
|
|
144
215
|
model: str | None = None,
|
|
145
216
|
region: str | None = None,
|
|
146
|
-
recorder=None,
|
|
147
217
|
event_handler=None,
|
|
218
|
+
event_handlers=None,
|
|
148
219
|
mcp_servers=None,
|
|
149
220
|
sandbox=False,
|
|
150
221
|
):
|
|
151
222
|
self.model = model
|
|
152
223
|
self.region = region
|
|
153
|
-
self.recorder = recorder
|
|
154
224
|
self.event_handler = event_handler
|
|
155
225
|
|
|
156
226
|
def generate(self, prompt: str, cwd=None, timeout=300, silent=False) -> str:
|
|
@@ -170,7 +240,7 @@ Notes:
|
|
|
170
240
|
- Registration is import-driven. Your provider is available only after the module defining it has been imported in the current Python process.
|
|
171
241
|
- `list_providers()` returns canonical provider names only. Aliases resolve via `get_provider_class(...)` and `CodingAgent(provider=...)`.
|
|
172
242
|
- `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`, `
|
|
243
|
+
- If you want `CodingAgent(...)` to instantiate your provider, its constructor should accept the shared kwargs `model`, `event_handler`, `event_handlers`, `mcp_servers`, and `sandbox` as needed.
|
|
174
244
|
- If your provider needs extra constructor arguments beyond the shared portable set, pass them via `backend_kwargs={...}` when constructing `CodingAgent(...)`.
|
|
175
245
|
|
|
176
246
|
## Development
|
|
@@ -2,6 +2,7 @@ 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
|
|
5
6
|
from .gemini import GeminiCodingAgent
|
|
6
7
|
from .mcp_config import HttpMcpServer, McpServerConfig, StdioMcpServer
|
|
7
8
|
from .opencode import OpencodeCodingAgent
|
|
@@ -14,6 +15,9 @@ __all__ = [
|
|
|
14
15
|
"get_provider_class",
|
|
15
16
|
"list_providers",
|
|
16
17
|
"register_provider",
|
|
18
|
+
"CompositeEventHandler",
|
|
19
|
+
"ConsoleEventHandler",
|
|
20
|
+
"NullEventHandler",
|
|
17
21
|
"CopilotCodingAgent",
|
|
18
22
|
"CodexCodingAgent",
|
|
19
23
|
"GeminiCodingAgent",
|
|
@@ -9,7 +9,6 @@ from typing import Any, TypeVar
|
|
|
9
9
|
from agentshim.events import AgentEventHandler
|
|
10
10
|
from agentshim.mcp_config import McpServerConfig
|
|
11
11
|
from agentshim.sandbox import SandboxConfig
|
|
12
|
-
from agentshim.trajectory import NullTrajectoryRecorder, TrajectoryRecorderProtocol
|
|
13
12
|
|
|
14
13
|
_T = TypeVar("_T")
|
|
15
14
|
_READABLE_NAME_BOUNDARY = re.compile(r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])")
|
|
@@ -28,7 +27,6 @@ def _readable_name_from_class_name(class_name: str) -> str:
|
|
|
28
27
|
class BaseCodingAgent(ABC):
|
|
29
28
|
"""Abstract base class for coding agents."""
|
|
30
29
|
|
|
31
|
-
recorder: TrajectoryRecorderProtocol = NullTrajectoryRecorder()
|
|
32
30
|
event_handler: Any | None = None
|
|
33
31
|
|
|
34
32
|
@property
|
|
@@ -113,9 +111,7 @@ class ProviderRegistry:
|
|
|
113
111
|
if not normalized:
|
|
114
112
|
raise ValueError("provider name must not be empty")
|
|
115
113
|
if not _PROVIDER_NAME_PATTERN.fullmatch(normalized):
|
|
116
|
-
raise ValueError(
|
|
117
|
-
f"invalid provider name '{name}'; use lowercase letters, digits, hyphens, or underscores"
|
|
118
|
-
)
|
|
114
|
+
raise ValueError(f"invalid provider name '{name}'; use lowercase letters, digits, hyphens, or underscores")
|
|
119
115
|
return normalized
|
|
120
116
|
|
|
121
117
|
def _normalize_names(self, canonical_name: str, aliases: tuple[str, ...]) -> tuple[str, tuple[str, ...]]:
|
|
@@ -147,9 +143,7 @@ class ProviderRegistry:
|
|
|
147
143
|
all_names = (canonical, *normalized_aliases)
|
|
148
144
|
|
|
149
145
|
collisions = [
|
|
150
|
-
name
|
|
151
|
-
for name in all_names
|
|
152
|
-
if name in self._providers and self._providers[name] is not provider_cls
|
|
146
|
+
name for name in all_names if name in self._providers and self._providers[name] is not provider_cls
|
|
153
147
|
]
|
|
154
148
|
if collisions and not overwrite:
|
|
155
149
|
raise ValueError(
|
|
@@ -172,18 +166,14 @@ class ProviderRegistry:
|
|
|
172
166
|
normalized = self._normalize_name(name)
|
|
173
167
|
provider_cls = self._providers.get(normalized)
|
|
174
168
|
if provider_cls is None:
|
|
175
|
-
raise ValueError(
|
|
176
|
-
f"Unknown coding agent provider '{name}'. Available providers: {self.list_providers()}"
|
|
177
|
-
)
|
|
169
|
+
raise ValueError(f"Unknown coding agent provider '{name}'. Available providers: {self.list_providers()}")
|
|
178
170
|
return provider_cls
|
|
179
171
|
|
|
180
172
|
def get_canonical_name(self, name: str) -> str:
|
|
181
173
|
normalized = self._normalize_name(name)
|
|
182
174
|
canonical = self._canonical_names.get(normalized)
|
|
183
175
|
if canonical is None:
|
|
184
|
-
raise ValueError(
|
|
185
|
-
f"Unknown coding agent provider '{name}'. Available providers: {self.list_providers()}"
|
|
186
|
-
)
|
|
176
|
+
raise ValueError(f"Unknown coding agent provider '{name}'. Available providers: {self.list_providers()}")
|
|
187
177
|
return canonical
|
|
188
178
|
|
|
189
179
|
|
|
@@ -243,8 +233,8 @@ class CodingAgent(BaseCodingAgent):
|
|
|
243
233
|
self,
|
|
244
234
|
provider: str,
|
|
245
235
|
model: str | None = None,
|
|
246
|
-
recorder: TrajectoryRecorderProtocol | None = None,
|
|
247
236
|
event_handler: AgentEventHandler | None = None,
|
|
237
|
+
event_handlers: Sequence[AgentEventHandler] | None = None,
|
|
248
238
|
mcp_servers: Sequence[McpServerConfig] | None = None,
|
|
249
239
|
sandbox: bool | SandboxConfig | None = False,
|
|
250
240
|
backend_kwargs: dict[str, Any] | None = None,
|
|
@@ -256,10 +246,10 @@ class CodingAgent(BaseCodingAgent):
|
|
|
256
246
|
portable_kwargs: dict[str, Any] = {}
|
|
257
247
|
if model is not None:
|
|
258
248
|
portable_kwargs["model"] = model
|
|
259
|
-
if recorder is not None:
|
|
260
|
-
portable_kwargs["recorder"] = recorder
|
|
261
249
|
if event_handler is not None:
|
|
262
250
|
portable_kwargs["event_handler"] = event_handler
|
|
251
|
+
if event_handlers is not None:
|
|
252
|
+
portable_kwargs["event_handlers"] = list(event_handlers)
|
|
263
253
|
if mcp_servers is not None:
|
|
264
254
|
portable_kwargs["mcp_servers"] = list(mcp_servers)
|
|
265
255
|
if sandbox is not None and sandbox is not False:
|
|
@@ -269,8 +259,7 @@ class CodingAgent(BaseCodingAgent):
|
|
|
269
259
|
overlapping_keys = sorted(portable_kwargs.keys() & advanced_kwargs.keys())
|
|
270
260
|
if overlapping_keys:
|
|
271
261
|
raise ValueError(
|
|
272
|
-
"backend_kwargs must not override portable CodingAgent arguments: "
|
|
273
|
-
+ ", ".join(overlapping_keys)
|
|
262
|
+
"backend_kwargs must not override portable CodingAgent arguments: " + ", ".join(overlapping_keys)
|
|
274
263
|
)
|
|
275
264
|
|
|
276
265
|
self._backend: BaseCodingAgent = provider_cls(**portable_kwargs, **advanced_kwargs)
|
|
@@ -296,14 +285,6 @@ class CodingAgent(BaseCodingAgent):
|
|
|
296
285
|
def model(self, value: Any) -> None:
|
|
297
286
|
self._backend.model = value # type: ignore[attr-defined]
|
|
298
287
|
|
|
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
288
|
@property
|
|
308
289
|
def event_handler(self) -> Any | None:
|
|
309
290
|
return getattr(self._backend, "event_handler", None)
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import subprocess
|
|
3
3
|
import time
|
|
4
|
-
from collections.abc import Callable
|
|
4
|
+
from collections.abc import Callable, Iterable
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
-
from agentshim.trajectory import TrajectoryRecorderProtocol
|
|
8
|
-
|
|
9
7
|
from ..base import register_provider
|
|
10
8
|
from ..cli_agent import CLICodingAgent, CLIGenerationSession
|
|
11
9
|
from ..events import AgentEventHandler
|
|
@@ -44,24 +42,22 @@ class ClaudeGenerationSession(CLIGenerationSession):
|
|
|
44
42
|
except json.JSONDecodeError:
|
|
45
43
|
# Fallback for non-JSON lines - still accumulate them
|
|
46
44
|
self.stdout_lines.append(line.rstrip())
|
|
47
|
-
if
|
|
48
|
-
|
|
49
|
-
self._log_raw(f"{self.log_prefix} ")
|
|
50
|
-
self._log_raw(line.rstrip() + "\n")
|
|
51
|
-
self._at_line_start = True
|
|
45
|
+
if line.rstrip():
|
|
46
|
+
self.event_handler.on_thinking(line.rstrip() + "\n")
|
|
52
47
|
|
|
53
48
|
def _handle_event(self, event: ClaudeEvent):
|
|
54
49
|
"""Handle a single parsed Claude event."""
|
|
55
50
|
if isinstance(event, MultiEvent):
|
|
51
|
+
if event.usage and self.event_handler is not None:
|
|
52
|
+
on_usage = getattr(self.event_handler, "on_usage", None)
|
|
53
|
+
if on_usage is not None:
|
|
54
|
+
on_usage(event.usage)
|
|
56
55
|
for sub_event in event.events:
|
|
57
56
|
self._handle_event(sub_event)
|
|
58
57
|
return
|
|
59
58
|
|
|
60
59
|
self._update_state(event)
|
|
61
60
|
|
|
62
|
-
if not self.silent:
|
|
63
|
-
self._render_event(event)
|
|
64
|
-
|
|
65
61
|
def _update_state(self, event: ClaudeEvent):
|
|
66
62
|
"""Update internal state based on the event."""
|
|
67
63
|
if isinstance(event, SystemEvent):
|
|
@@ -88,14 +84,7 @@ class ClaudeGenerationSession(CLIGenerationSession):
|
|
|
88
84
|
|
|
89
85
|
start_time = self.tool_start_times.get(event.tool_id)
|
|
90
86
|
duration = time.time() - start_time if start_time else None
|
|
91
|
-
args = self.tool_args.get(event.tool_id, {})
|
|
92
87
|
|
|
93
|
-
self.recorder.add_tool_call(
|
|
94
|
-
tool=event.tool_name_resolved,
|
|
95
|
-
args=args,
|
|
96
|
-
stdout=event.output,
|
|
97
|
-
duration=duration,
|
|
98
|
-
)
|
|
99
88
|
if self.event_handler:
|
|
100
89
|
self.event_handler.on_tool_result(
|
|
101
90
|
tool=event.tool_name_resolved,
|
|
@@ -105,6 +94,9 @@ class ClaudeGenerationSession(CLIGenerationSession):
|
|
|
105
94
|
|
|
106
95
|
elif isinstance(event, ResultEvent):
|
|
107
96
|
self.final_result = event.result
|
|
97
|
+
self.final_usage = event.usage
|
|
98
|
+
self.total_cost_usd = event.total_cost_usd
|
|
99
|
+
self.duration_ms = event.duration_ms
|
|
108
100
|
# Anthropic reports cache_creation + cache_read as disjoint
|
|
109
101
|
# from input_tokens; fold them into input_tokens to match the
|
|
110
102
|
# crucible invariant (cached ⊆ input).
|
|
@@ -121,20 +113,6 @@ class ClaudeGenerationSession(CLIGenerationSession):
|
|
|
121
113
|
provider="claude",
|
|
122
114
|
)
|
|
123
115
|
|
|
124
|
-
def _render_event(self, event: ClaudeEvent):
|
|
125
|
-
"""Render the event to stdout."""
|
|
126
|
-
if isinstance(event, TextEvent):
|
|
127
|
-
self._print_stream_content(event.text)
|
|
128
|
-
return
|
|
129
|
-
|
|
130
|
-
if not self._at_line_start:
|
|
131
|
-
self._log_raw("\n")
|
|
132
|
-
self._at_line_start = True
|
|
133
|
-
|
|
134
|
-
output = event.render(self.log_prefix)
|
|
135
|
-
if output:
|
|
136
|
-
self._log_raw(output + "\n")
|
|
137
|
-
|
|
138
116
|
def run(self, prompt: str) -> str:
|
|
139
117
|
"""Execute the command and return the result."""
|
|
140
118
|
super().run(prompt)
|
|
@@ -150,8 +128,8 @@ class ClaudeCodeCodingAgent(CLICodingAgent):
|
|
|
150
128
|
def __init__(
|
|
151
129
|
self,
|
|
152
130
|
model: str | None = None,
|
|
153
|
-
recorder: TrajectoryRecorderProtocol | None = None,
|
|
154
131
|
event_handler: AgentEventHandler | None = None,
|
|
132
|
+
event_handlers: Iterable[AgentEventHandler] | None = None,
|
|
155
133
|
mcp_servers: list[McpServerConfig] | None = None,
|
|
156
134
|
sandbox: bool | SandboxConfig = False,
|
|
157
135
|
):
|
|
@@ -159,7 +137,6 @@ class ClaudeCodeCodingAgent(CLICodingAgent):
|
|
|
159
137
|
|
|
160
138
|
Args:
|
|
161
139
|
model: Optional model name to use with Claude Code. If None, uses default.
|
|
162
|
-
recorder: Trajectory recorder instance.
|
|
163
140
|
event_handler: Optional event handler for UI updates.
|
|
164
141
|
mcp_servers: Optional list of MCP server configurations.
|
|
165
142
|
sandbox: If True (or a ``SandboxConfig``), enable Claude Code's
|
|
@@ -169,7 +146,7 @@ class ClaudeCodeCodingAgent(CLICodingAgent):
|
|
|
169
146
|
sandboxed; the Claude process itself is not wrapped.
|
|
170
147
|
Defaults to False (no sandbox).
|
|
171
148
|
"""
|
|
172
|
-
super().__init__("claude", model,
|
|
149
|
+
super().__init__("claude", model, event_handler, event_handlers, mcp_servers)
|
|
173
150
|
self.sandbox = resolve_sandbox(sandbox)
|
|
174
151
|
if self.sandbox is not None:
|
|
175
152
|
# Without this, Claude Code cd's into a per-invocation scratch dir
|
|
@@ -246,7 +223,6 @@ class ClaudeCodeCodingAgent(CLICodingAgent):
|
|
|
246
223
|
cwd: str | None = None,
|
|
247
224
|
timeout: int = 300,
|
|
248
225
|
silent: bool = False,
|
|
249
|
-
recorder: TrajectoryRecorderProtocol | None = None,
|
|
250
226
|
on_process_started: Callable[[subprocess.Popen[str]], None] | None = None,
|
|
251
227
|
) -> ClaudeGenerationSession:
|
|
252
228
|
return ClaudeGenerationSession(
|
|
@@ -258,7 +234,6 @@ class ClaudeCodeCodingAgent(CLICodingAgent):
|
|
|
258
234
|
cwd=cwd,
|
|
259
235
|
timeout=timeout,
|
|
260
236
|
silent=silent,
|
|
261
|
-
recorder=recorder,
|
|
262
237
|
event_handler=self.event_handler,
|
|
263
238
|
on_process_started=on_process_started,
|
|
264
239
|
)
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
3
|
from typing import Any, cast
|
|
5
4
|
|
|
6
|
-
from ..utils import truncate_content, truncate_tool_params
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
class ClaudeEvent(ABC):
|
|
6
|
+
class ClaudeEvent:
|
|
10
7
|
"""Base class for Claude Code stream events."""
|
|
11
8
|
|
|
12
|
-
@abstractmethod
|
|
13
|
-
def render(self, log_prefix: str) -> str | None:
|
|
14
|
-
"""Render the event as a string for terminal output."""
|
|
15
|
-
|
|
16
9
|
@staticmethod
|
|
17
10
|
def from_dict(data: dict[str, Any]) -> ClaudeEvent | None:
|
|
18
11
|
"""Factory method to create events from JSON data."""
|
|
@@ -36,7 +29,8 @@ class ClaudeEvent(ABC):
|
|
|
36
29
|
parameters=block.get("input"),
|
|
37
30
|
)
|
|
38
31
|
)
|
|
39
|
-
|
|
32
|
+
usage = message.get("usage")
|
|
33
|
+
return MultiEvent(events, usage=usage) if events else None
|
|
40
34
|
if event_type == "user":
|
|
41
35
|
message = data.get("message", {})
|
|
42
36
|
content_blocks = message.get("content", [])
|
|
@@ -53,6 +47,7 @@ class ClaudeEvent(ABC):
|
|
|
53
47
|
num_turns=data.get("num_turns"),
|
|
54
48
|
usage=data.get("usage"),
|
|
55
49
|
total_cost_usd=data.get("total_cost_usd"),
|
|
50
|
+
duration_ms=data.get("duration_ms"),
|
|
56
51
|
)
|
|
57
52
|
|
|
58
53
|
return None
|
|
@@ -61,12 +56,13 @@ class ClaudeEvent(ABC):
|
|
|
61
56
|
class MultiEvent(ClaudeEvent):
|
|
62
57
|
"""Container for multiple events from a single message."""
|
|
63
58
|
|
|
64
|
-
def __init__(
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
events: list[ClaudeEvent],
|
|
62
|
+
usage: dict[str, Any] | None = None,
|
|
63
|
+
):
|
|
65
64
|
self.events = events
|
|
66
|
-
|
|
67
|
-
def render(self, log_prefix: str) -> str | None:
|
|
68
|
-
# MultiEvent doesn't render itself; events are handled individually
|
|
69
|
-
return None
|
|
65
|
+
self.usage = usage
|
|
70
66
|
|
|
71
67
|
|
|
72
68
|
class SystemEvent(ClaudeEvent):
|
|
@@ -80,10 +76,6 @@ class SystemEvent(ClaudeEvent):
|
|
|
80
76
|
self.data = data
|
|
81
77
|
self.session_id: str | None = data.get("session_id")
|
|
82
78
|
|
|
83
|
-
def render(self, log_prefix: str) -> str | None:
|
|
84
|
-
# System events are silent
|
|
85
|
-
return None
|
|
86
|
-
|
|
87
79
|
|
|
88
80
|
class TextEvent(ClaudeEvent):
|
|
89
81
|
"""Assistant text content event."""
|
|
@@ -91,10 +83,6 @@ class TextEvent(ClaudeEvent):
|
|
|
91
83
|
def __init__(self, text: str):
|
|
92
84
|
self.text = text
|
|
93
85
|
|
|
94
|
-
def render(self, log_prefix: str) -> str | None:
|
|
95
|
-
# Text rendering is handled specially due to streaming
|
|
96
|
-
return self.text
|
|
97
|
-
|
|
98
86
|
|
|
99
87
|
class ToolUseEvent(ClaudeEvent):
|
|
100
88
|
"""Tool call event from assistant."""
|
|
@@ -104,10 +92,6 @@ class ToolUseEvent(ClaudeEvent):
|
|
|
104
92
|
self.tool_id = tool_id
|
|
105
93
|
self.parameters = parameters
|
|
106
94
|
|
|
107
|
-
def render(self, log_prefix: str) -> str:
|
|
108
|
-
truncated = truncate_tool_params(self.tool_name, self.parameters)
|
|
109
|
-
return f"{log_prefix} \033[34m[Tool Use] {self.tool_name} {truncated}\033[0m"
|
|
110
|
-
|
|
111
95
|
|
|
112
96
|
class ToolResultEvent(ClaudeEvent):
|
|
113
97
|
"""Tool execution result event."""
|
|
@@ -122,12 +106,6 @@ class ToolResultEvent(ClaudeEvent):
|
|
|
122
106
|
self.tool_id = tool_id
|
|
123
107
|
self.tool_name_resolved: str = "Tool" # To be set externally
|
|
124
108
|
|
|
125
|
-
def render(self, log_prefix: str) -> str:
|
|
126
|
-
if not self.output:
|
|
127
|
-
return f"{log_prefix} \033[32m{self.tool_name_resolved} ran successfully\033[0m"
|
|
128
|
-
truncated = truncate_content(self.output)
|
|
129
|
-
return f"{log_prefix} \033[32m[Tool Result] {truncated}\033[0m"
|
|
130
|
-
|
|
131
109
|
|
|
132
110
|
class ResultEvent(ClaudeEvent):
|
|
133
111
|
"""Final session summary event."""
|
|
@@ -138,12 +116,10 @@ class ResultEvent(ClaudeEvent):
|
|
|
138
116
|
num_turns: int | None = None,
|
|
139
117
|
usage: dict[str, Any] | None = None,
|
|
140
118
|
total_cost_usd: float | None = None,
|
|
119
|
+
duration_ms: int | None = None,
|
|
141
120
|
):
|
|
142
121
|
self.result = result
|
|
143
122
|
self.num_turns = num_turns
|
|
144
123
|
self.usage = usage
|
|
145
124
|
self.total_cost_usd = total_cost_usd
|
|
146
|
-
|
|
147
|
-
def render(self, log_prefix: str) -> str | None:
|
|
148
|
-
# Result events are silent (result is captured separately)
|
|
149
|
-
return None
|
|
125
|
+
self.duration_ms = duration_ms
|