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