agentshim 0.2.0__tar.gz → 0.3.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 (64) hide show
  1. {agentshim-0.2.0 → agentshim-0.3.0}/PKG-INFO +1 -1
  2. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/__init__.py +2 -0
  3. agentshim-0.3.0/agentshim/copilot/__init__.py +3 -0
  4. agentshim-0.3.0/agentshim/copilot/agent.py +290 -0
  5. agentshim-0.3.0/agentshim/copilot/events.py +289 -0
  6. agentshim-0.3.0/agentshim/copilot_events.py +29 -0
  7. {agentshim-0.2.0 → agentshim-0.3.0}/pyproject.toml +1 -1
  8. agentshim-0.3.0/tests/fixtures/copilot/session_turn_1.jsonl +11 -0
  9. agentshim-0.3.0/tests/fixtures/copilot/session_turn_2_resumed.jsonl +11 -0
  10. agentshim-0.3.0/tests/fixtures/copilot/streaming_dedup.jsonl +5 -0
  11. agentshim-0.3.0/tests/fixtures/copilot/tool_and_usage.jsonl +6 -0
  12. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/cli_agent/test_cli_prompt_passing.py +26 -1
  13. agentshim-0.3.0/tests/unit/test_agent_cli_copilot.py +168 -0
  14. agentshim-0.3.0/tests/unit/test_agent_cli_copilot_fixtures.py +99 -0
  15. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_agent_cli_event_parsing.py +65 -0
  16. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_agent_cli_resume.py +59 -0
  17. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_cli_agent_usage.py +37 -0
  18. {agentshim-0.2.0 → agentshim-0.3.0}/uv.lock +1 -1
  19. {agentshim-0.2.0 → agentshim-0.3.0}/.github/workflows/ci.yml +0 -0
  20. {agentshim-0.2.0 → agentshim-0.3.0}/.github/workflows/publish.yml +0 -0
  21. {agentshim-0.2.0 → agentshim-0.3.0}/.gitignore +0 -0
  22. {agentshim-0.2.0 → agentshim-0.3.0}/README.md +0 -0
  23. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/base.py +0 -0
  24. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/claude/__init__.py +0 -0
  25. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/claude/agent.py +0 -0
  26. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/claude/events.py +0 -0
  27. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/claude/hooks/__init__.py +0 -0
  28. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/claude/hooks/confine_reads.py +0 -0
  29. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/claude_events.py +0 -0
  30. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/cli_agent.py +0 -0
  31. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/codex/__init__.py +0 -0
  32. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/codex/agent.py +0 -0
  33. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/codex/events.py +0 -0
  34. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/codex_events.py +0 -0
  35. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/events.py +0 -0
  36. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/gemini/__init__.py +0 -0
  37. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/gemini/agent.py +0 -0
  38. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/gemini/events.py +0 -0
  39. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/gemini_events.py +0 -0
  40. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/llm_client.py +0 -0
  41. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/mcp_config.py +0 -0
  42. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/opencode/__init__.py +0 -0
  43. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/opencode/agent.py +0 -0
  44. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/opencode/events.py +0 -0
  45. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/opencode_events.py +0 -0
  46. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/py.typed +0 -0
  47. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/sandbox.py +0 -0
  48. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/subagent.py +0 -0
  49. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/trajectory.py +0 -0
  50. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/usage.py +0 -0
  51. {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/utils.py +0 -0
  52. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/cli_agent/test_agent_cli_cleanup.py +0 -0
  53. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/cli_agent/test_check_cli.py +0 -0
  54. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/llm/conftest.py +0 -0
  55. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/llm/test_claude_stream.py +0 -0
  56. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/llm/test_gemini_fixture.py +0 -0
  57. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/llm/test_gemini_stream.py +0 -0
  58. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_agent_cli_claude.py +0 -0
  59. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_agent_cli_codex.py +0 -0
  60. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_agent_cli_mcp_unsupported.py +0 -0
  61. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_agent_cli_sandbox.py +0 -0
  62. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_coding_agent_facade.py +0 -0
  63. {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_coding_agent_recorder_default.py +0 -0
  64. {agentshim-0.2.0 → agentshim-0.3.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.2.0
3
+ Version: 0.3.0
4
4
  Summary: Provider-agnostic coding-agent CLI shims
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: litellm>=1.0.0
@@ -1,5 +1,6 @@
1
1
  from .base import BaseAgentSession, BaseCodingAgent, CodingAgent, get_provider_class, list_providers, register_provider
2
2
  from .claude import ClaudeCodeCodingAgent
3
+ from .copilot import CopilotCodingAgent
3
4
  from .codex import CodexCodingAgent
4
5
  from .gemini import GeminiCodingAgent
5
6
  from .mcp_config import HttpMcpServer, McpServerConfig, StdioMcpServer
@@ -13,6 +14,7 @@ __all__ = [
13
14
  "get_provider_class",
14
15
  "list_providers",
15
16
  "register_provider",
17
+ "CopilotCodingAgent",
16
18
  "CodexCodingAgent",
17
19
  "GeminiCodingAgent",
18
20
  "OpencodeCodingAgent",
@@ -0,0 +1,3 @@
1
+ from .agent import CopilotCodingAgent, CopilotGenerationSession
2
+
3
+ __all__ = ["CopilotCodingAgent", "CopilotGenerationSession"]
@@ -0,0 +1,290 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+ import time
6
+ from collections.abc import Callable
7
+ from typing import Any
8
+
9
+ from agentshim.trajectory import TrajectoryRecorderProtocol
10
+
11
+ from ..base import register_provider
12
+ from ..cli_agent import CLICodingAgent, CLIGenerationSession
13
+ from ..events import AgentEventHandler
14
+ from ..mcp_config import HttpMcpServer, McpServerConfig
15
+ from ..sandbox import SandboxConfig
16
+ from ..usage import ProviderUsage, TokenUsage
17
+ from .events import (
18
+ CopilotEvent,
19
+ ErrorEvent,
20
+ IntentEvent,
21
+ MessageDeltaEvent,
22
+ MessageEvent,
23
+ ResultEvent,
24
+ SessionStartEvent,
25
+ ToolResultEvent,
26
+ ToolUseEvent,
27
+ TurnEndEvent,
28
+ UsageEvent,
29
+ )
30
+
31
+
32
+ class CopilotGenerationSession(CLIGenerationSession):
33
+ """Session that parses Copilot CLI JSONL events."""
34
+
35
+ def __init__(self, **kwargs: Any):
36
+ super().__init__(**kwargs)
37
+ self.tool_map: dict[str, str] = {}
38
+ self.tool_start_times: dict[str, float] = {}
39
+ self.tool_args: dict[str, Any] = {}
40
+ self.final_result: str | None = None
41
+ self._seen_message_deltas: set[str] = set()
42
+ self._streamed_text_chunks: list[str] = []
43
+ self._accumulated_tokens = TokenUsage()
44
+ self._turn_count = 0
45
+ self._message_output_tokens = 0
46
+ self._saw_usage_event = False
47
+
48
+ def _refresh_usage(self) -> None:
49
+ effective_output_tokens = self._accumulated_tokens.output_tokens
50
+ if not self._saw_usage_event:
51
+ effective_output_tokens = self._message_output_tokens
52
+ self.usage = ProviderUsage(
53
+ tokens=TokenUsage(
54
+ input_tokens=self._accumulated_tokens.input_tokens,
55
+ output_tokens=effective_output_tokens,
56
+ cached_input_tokens=self._accumulated_tokens.cached_input_tokens,
57
+ turns=self._turn_count,
58
+ ),
59
+ total_cost_usd=None,
60
+ provider="copilot",
61
+ )
62
+
63
+ def _process_stdout(self, line: str) -> None:
64
+ if not line:
65
+ return
66
+ try:
67
+ data = json.loads(line)
68
+ except json.JSONDecodeError:
69
+ self.stdout_lines.append(line.rstrip())
70
+ if not self.silent:
71
+ if self._at_line_start:
72
+ self._log_raw(f"{self.log_prefix} ")
73
+ self._log_raw(line.rstrip() + "\n")
74
+ self._at_line_start = True
75
+ return
76
+
77
+ if not isinstance(data, dict):
78
+ return
79
+
80
+ event = CopilotEvent.from_dict(data)
81
+ if event is None:
82
+ return
83
+ self._handle_event(event)
84
+
85
+ def _handle_event(self, event: CopilotEvent) -> None:
86
+ self._update_state(event)
87
+ if not self.silent:
88
+ self._render_event(event)
89
+
90
+ def _update_state(self, event: CopilotEvent) -> None:
91
+ if isinstance(event, SessionStartEvent):
92
+ if self.session_id is None and event.session_id:
93
+ self.session_id = event.session_id
94
+ return
95
+
96
+ if isinstance(event, MessageDeltaEvent):
97
+ if event.message_id:
98
+ self._seen_message_deltas.add(event.message_id)
99
+ if event.delta_content:
100
+ self._streamed_text_chunks.append(event.delta_content)
101
+ if self.event_handler:
102
+ self.event_handler.on_thinking(event.delta_content)
103
+ return
104
+
105
+ if isinstance(event, MessageEvent):
106
+ if event.content:
107
+ self.stdout_lines.append(event.content)
108
+ self.final_result = event.content
109
+ if not self._saw_usage_event:
110
+ self._message_output_tokens += event.output_tokens
111
+ self._refresh_usage()
112
+ if self.event_handler:
113
+ self.event_handler.on_thinking(event.content)
114
+ return
115
+
116
+ if isinstance(event, ToolUseEvent):
117
+ if event.tool_id:
118
+ self.tool_map[event.tool_id] = event.tool_name
119
+ self.tool_start_times[event.tool_id] = time.time()
120
+ self.tool_args[event.tool_id] = event.arguments
121
+ if self.event_handler:
122
+ self.event_handler.on_tool_call(event.tool_name, event.arguments)
123
+ return
124
+
125
+ if isinstance(event, ToolResultEvent):
126
+ if not event.tool_id:
127
+ return
128
+ event.tool_name_resolved = self.tool_map.get(event.tool_id, "Tool")
129
+ start_time = self.tool_start_times.get(event.tool_id)
130
+ duration = time.time() - start_time if start_time else None
131
+ args = self.tool_args.get(event.tool_id, {})
132
+ stdout = event.output or event.error_message
133
+
134
+ self.recorder.add_tool_call(
135
+ tool=event.tool_name_resolved,
136
+ args=args,
137
+ stdout=stdout,
138
+ exit_code=event.exit_code,
139
+ duration=duration,
140
+ )
141
+ if self.event_handler:
142
+ self.event_handler.on_tool_result(
143
+ tool=event.tool_name_resolved,
144
+ stdout=stdout,
145
+ exit_code=event.exit_code,
146
+ duration=duration,
147
+ )
148
+ return
149
+
150
+ if isinstance(event, UsageEvent):
151
+ self._saw_usage_event = True
152
+ cached = event.cache_read_tokens + event.cache_write_tokens
153
+ self._accumulated_tokens = self._accumulated_tokens + TokenUsage(
154
+ input_tokens=event.input_tokens + cached,
155
+ output_tokens=event.output_tokens + event.reasoning_tokens,
156
+ cached_input_tokens=cached,
157
+ turns=0,
158
+ )
159
+ self._refresh_usage()
160
+ return
161
+
162
+ if isinstance(event, TurnEndEvent):
163
+ self._turn_count += 1
164
+ self._refresh_usage()
165
+ return
166
+
167
+ if isinstance(event, ResultEvent):
168
+ if self.session_id is None and event.session_id:
169
+ self.session_id = event.session_id
170
+ return
171
+
172
+ if isinstance(event, ErrorEvent):
173
+ if event.message:
174
+ self.stdout_lines.append(event.message)
175
+
176
+ def _render_event(self, event: CopilotEvent) -> None:
177
+ if isinstance(event, MessageDeltaEvent):
178
+ if event.delta_content:
179
+ self._print_stream_content(event.delta_content)
180
+ return
181
+
182
+ if isinstance(event, MessageEvent):
183
+ if event.message_id and event.message_id in self._seen_message_deltas:
184
+ return
185
+ if event.content:
186
+ self._print_stream_content(event.content)
187
+ return
188
+
189
+ if not self._at_line_start:
190
+ self._log_raw("\n")
191
+ self._at_line_start = True
192
+
193
+ if isinstance(event, IntentEvent):
194
+ output = event.render(self.log_prefix)
195
+ else:
196
+ output = event.render(self.log_prefix)
197
+ if output:
198
+ self._log_raw(output + "\n")
199
+
200
+ def run(self, prompt: str) -> str:
201
+ super().run(prompt)
202
+ if self.final_result:
203
+ return self.final_result
204
+ if self._streamed_text_chunks:
205
+ return "".join(self._streamed_text_chunks).strip()
206
+ return "\n".join(self.stdout_lines)
207
+
208
+
209
+ @register_provider("copilot", "github-copilot", "copilot-cli")
210
+ class CopilotCodingAgent(CLICodingAgent):
211
+ """Coding agent implementation using the GitHub Copilot CLI tool."""
212
+
213
+ def __init__(
214
+ self,
215
+ model: str | None = None,
216
+ recorder: TrajectoryRecorderProtocol | None = None,
217
+ event_handler: AgentEventHandler | None = None,
218
+ mcp_servers: list[McpServerConfig] | None = None,
219
+ sandbox: bool | SandboxConfig = False,
220
+ ):
221
+ if sandbox:
222
+ raise NotImplementedError("sandbox is not supported for CopilotCodingAgent")
223
+ super().__init__("copilot", model, recorder, event_handler, mcp_servers)
224
+
225
+ @property
226
+ def copilot_path(self) -> str:
227
+ """Return path to copilot binary (for backward compatibility)."""
228
+ return self.binary_path
229
+
230
+ @property
231
+ def _log_prefix(self) -> str:
232
+ return "[Copilot]"
233
+
234
+ def _build_mcp_config_json(self) -> str:
235
+ servers: dict[str, dict[str, Any]] = {}
236
+ for server in self.mcp_servers:
237
+ if isinstance(server, HttpMcpServer):
238
+ entry: dict[str, Any] = {"type": "sse", "url": server.url}
239
+ if server.headers:
240
+ entry["headers"] = dict(server.headers)
241
+ servers[server.name] = entry
242
+ else:
243
+ entry = {"command": server.command, "args": server.args}
244
+ if server.env:
245
+ entry["env"] = server.env
246
+ servers[server.name] = entry
247
+ return json.dumps({"mcpServers": servers})
248
+
249
+ def _get_command(self, prompt: str, resume_session_id: str | None = None) -> list[str]:
250
+ cmd = [
251
+ self.binary_path,
252
+ "--output-format",
253
+ "json",
254
+ "--stream",
255
+ "off",
256
+ "--allow-all-tools",
257
+ "--allow-all-paths",
258
+ "--allow-all-urls",
259
+ ]
260
+ if resume_session_id:
261
+ cmd.extend(["--resume", resume_session_id])
262
+ if self.model:
263
+ cmd.extend(["--model", self.model])
264
+ if self.mcp_servers:
265
+ cmd.extend(["--additional-mcp-config", self._build_mcp_config_json()])
266
+ cmd.extend(["-p", prompt])
267
+ return cmd
268
+
269
+ def _create_session(
270
+ self,
271
+ cmd: list[str],
272
+ cwd: str | None = None,
273
+ timeout: int = 300,
274
+ silent: bool = False,
275
+ recorder: TrajectoryRecorderProtocol | None = None,
276
+ on_process_started: Callable[[subprocess.Popen[str]], None] | None = None,
277
+ ) -> CopilotGenerationSession:
278
+ return CopilotGenerationSession(
279
+ binary_name=self.binary_name,
280
+ env=self.env,
281
+ log_prefix=self._log_prefix,
282
+ cmd=cmd,
283
+ logger=self.logger,
284
+ cwd=cwd,
285
+ timeout=timeout,
286
+ silent=silent,
287
+ recorder=recorder,
288
+ event_handler=self.event_handler,
289
+ on_process_started=on_process_started,
290
+ )
@@ -0,0 +1,289 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, cast
5
+
6
+ from ..utils import truncate_content, truncate_tool_params
7
+
8
+
9
+ def _as_dict(value: Any) -> dict[str, Any]:
10
+ return cast("dict[str, Any]", value) if isinstance(value, dict) else {}
11
+
12
+
13
+ def _extract_tool_result(result_raw: Any) -> tuple[str, int | None]:
14
+ result = _as_dict(result_raw)
15
+ exit_code: int | None = None
16
+
17
+ detailed = result.get("detailedContent")
18
+ if isinstance(detailed, str) and detailed:
19
+ output = detailed
20
+ else:
21
+ content = result.get("content")
22
+ output = content if isinstance(content, str) else ""
23
+
24
+ contents = result.get("contents")
25
+ if isinstance(contents, list):
26
+ rendered_blocks: list[str] = []
27
+ for item_raw in contents:
28
+ item = _as_dict(item_raw)
29
+ item_type = item.get("type")
30
+ if item_type in ("text", "terminal"):
31
+ text = item.get("text")
32
+ if isinstance(text, str) and text:
33
+ rendered_blocks.append(text)
34
+ if item_type == "terminal":
35
+ candidate_exit_code = item.get("exitCode")
36
+ if isinstance(candidate_exit_code, int):
37
+ exit_code = candidate_exit_code
38
+ elif item_type == "resource_link":
39
+ name = item.get("title") or item.get("name") or item.get("uri")
40
+ uri = item.get("uri")
41
+ if isinstance(name, str) and isinstance(uri, str):
42
+ rendered_blocks.append(f"{name}: {uri}")
43
+ if not output and rendered_blocks:
44
+ output = "\n".join(rendered_blocks)
45
+
46
+ return output, exit_code
47
+
48
+
49
+ class CopilotEvent(ABC):
50
+ """Base class for Copilot CLI JSONL events."""
51
+
52
+ @abstractmethod
53
+ def render(self, log_prefix: str) -> str | None:
54
+ """Render the event as a string for terminal output."""
55
+
56
+ @staticmethod
57
+ def from_dict(data: dict[str, Any]) -> CopilotEvent | None:
58
+ event_type = data.get("type")
59
+ payload = _as_dict(data.get("data"))
60
+
61
+ if event_type == "session.start":
62
+ session_id_raw = payload.get("sessionId")
63
+ session_id = session_id_raw if isinstance(session_id_raw, str) else None
64
+ return SessionStartEvent(session_id=session_id)
65
+
66
+ if event_type == "assistant.intent":
67
+ intent = payload.get("intent")
68
+ if isinstance(intent, str):
69
+ return IntentEvent(intent=intent)
70
+ return None
71
+
72
+ if event_type == "assistant.message":
73
+ message_id_raw = payload.get("messageId")
74
+ message_id = message_id_raw if isinstance(message_id_raw, str) else None
75
+ content_raw = payload.get("content")
76
+ content = content_raw if isinstance(content_raw, str) else ""
77
+ output_tokens_raw = payload.get("outputTokens")
78
+ output_tokens = output_tokens_raw if isinstance(output_tokens_raw, int) else 0
79
+ return MessageEvent(message_id=message_id, content=content, output_tokens=output_tokens)
80
+
81
+ if event_type == "assistant.message_delta":
82
+ message_id_raw = payload.get("messageId")
83
+ message_id = message_id_raw if isinstance(message_id_raw, str) else None
84
+ delta_raw = payload.get("deltaContent")
85
+ delta = delta_raw if isinstance(delta_raw, str) else ""
86
+ return MessageDeltaEvent(message_id=message_id, delta_content=delta)
87
+
88
+ if event_type == "assistant.turn_end":
89
+ turn_id_raw = payload.get("turnId")
90
+ turn_id = turn_id_raw if isinstance(turn_id_raw, str) else None
91
+ return TurnEndEvent(turn_id=turn_id)
92
+
93
+ if event_type == "assistant.usage":
94
+ model_raw = payload.get("model")
95
+ model = model_raw if isinstance(model_raw, str) else ""
96
+ return UsageEvent(
97
+ model=model,
98
+ input_tokens=int(payload.get("inputTokens") or 0),
99
+ output_tokens=int(payload.get("outputTokens") or 0),
100
+ cache_read_tokens=int(payload.get("cacheReadTokens") or 0),
101
+ cache_write_tokens=int(payload.get("cacheWriteTokens") or 0),
102
+ reasoning_tokens=int(payload.get("reasoningTokens") or 0),
103
+ )
104
+
105
+ if event_type == "tool.execution_start":
106
+ tool_id_raw = payload.get("toolCallId")
107
+ tool_id = tool_id_raw if isinstance(tool_id_raw, str) else None
108
+ tool_name_raw = payload.get("toolName")
109
+ tool_name = tool_name_raw if isinstance(tool_name_raw, str) else "Tool"
110
+ return ToolUseEvent(
111
+ tool_id=tool_id,
112
+ tool_name=tool_name,
113
+ arguments=payload.get("arguments"),
114
+ )
115
+
116
+ if event_type == "tool.execution_complete":
117
+ tool_id_raw = payload.get("toolCallId")
118
+ tool_id = tool_id_raw if isinstance(tool_id_raw, str) else None
119
+ success = bool(payload.get("success"))
120
+ output, exit_code = _extract_tool_result(payload.get("result"))
121
+ error = _as_dict(payload.get("error"))
122
+ error_message_raw = error.get("message")
123
+ error_message = error_message_raw if isinstance(error_message_raw, str) else ""
124
+ return ToolResultEvent(
125
+ tool_id=tool_id,
126
+ success=success,
127
+ output=output,
128
+ error_message=error_message,
129
+ exit_code=exit_code,
130
+ )
131
+
132
+ if event_type == "session.error":
133
+ error_type_raw = payload.get("errorType")
134
+ error_type = error_type_raw if isinstance(error_type_raw, str) else None
135
+ message_raw = payload.get("message")
136
+ message = message_raw if isinstance(message_raw, str) else ""
137
+ return ErrorEvent(message=message, error_type=error_type)
138
+
139
+ if event_type == "result":
140
+ session_id_raw = data.get("sessionId")
141
+ session_id = session_id_raw if isinstance(session_id_raw, str) else None
142
+ exit_code_raw = data.get("exitCode")
143
+ exit_code = exit_code_raw if isinstance(exit_code_raw, int) else None
144
+ usage = _as_dict(data.get("usage"))
145
+ return ResultEvent(session_id=session_id, exit_code=exit_code, usage=usage)
146
+
147
+ return None
148
+
149
+
150
+ class SessionStartEvent(CopilotEvent):
151
+ """Session start event carrying the resumable session id."""
152
+
153
+ def __init__(self, session_id: str | None):
154
+ self.session_id = session_id
155
+
156
+ def render(self, log_prefix: str) -> str | None:
157
+ return None
158
+
159
+
160
+ class IntentEvent(CopilotEvent):
161
+ """Ephemeral agent intent update."""
162
+
163
+ def __init__(self, intent: str):
164
+ self.intent = intent
165
+
166
+ def render(self, log_prefix: str) -> str:
167
+ return f"{log_prefix} \033[36m[Intent] {self.intent}\033[0m"
168
+
169
+
170
+ class MessageEvent(CopilotEvent):
171
+ """Final assistant message content."""
172
+
173
+ def __init__(self, message_id: str | None, content: str, output_tokens: int = 0):
174
+ self.message_id = message_id
175
+ self.content = content
176
+ self.output_tokens = output_tokens
177
+
178
+ def render(self, log_prefix: str) -> str | None:
179
+ return self.content
180
+
181
+
182
+ class MessageDeltaEvent(CopilotEvent):
183
+ """Streaming assistant message delta."""
184
+
185
+ def __init__(self, message_id: str | None, delta_content: str):
186
+ self.message_id = message_id
187
+ self.delta_content = delta_content
188
+
189
+ def render(self, log_prefix: str) -> str | None:
190
+ return self.delta_content
191
+
192
+
193
+ class TurnEndEvent(CopilotEvent):
194
+ """Turn completion marker."""
195
+
196
+ def __init__(self, turn_id: str | None):
197
+ self.turn_id = turn_id
198
+
199
+ def render(self, log_prefix: str) -> str | None:
200
+ return None
201
+
202
+
203
+ class UsageEvent(CopilotEvent):
204
+ """Per-request usage summary."""
205
+
206
+ def __init__(
207
+ self,
208
+ model: str,
209
+ input_tokens: int = 0,
210
+ output_tokens: int = 0,
211
+ cache_read_tokens: int = 0,
212
+ cache_write_tokens: int = 0,
213
+ reasoning_tokens: int = 0,
214
+ ):
215
+ self.model = model
216
+ self.input_tokens = input_tokens
217
+ self.output_tokens = output_tokens
218
+ self.cache_read_tokens = cache_read_tokens
219
+ self.cache_write_tokens = cache_write_tokens
220
+ self.reasoning_tokens = reasoning_tokens
221
+
222
+ def render(self, log_prefix: str) -> str | None:
223
+ return None
224
+
225
+
226
+ class ToolUseEvent(CopilotEvent):
227
+ """Tool execution start event."""
228
+
229
+ def __init__(self, tool_id: str | None, tool_name: str, arguments: Any):
230
+ self.tool_id = tool_id
231
+ self.tool_name = tool_name
232
+ self.arguments = arguments
233
+
234
+ def render(self, log_prefix: str) -> str:
235
+ truncated = truncate_tool_params(self.tool_name, self.arguments)
236
+ return f"{log_prefix} \033[34m[Tool Use] {self.tool_name} {truncated}\033[0m"
237
+
238
+
239
+ class ToolResultEvent(CopilotEvent):
240
+ """Tool execution completion event."""
241
+
242
+ def __init__(
243
+ self,
244
+ tool_id: str | None,
245
+ success: bool,
246
+ output: str = "",
247
+ error_message: str = "",
248
+ exit_code: int | None = None,
249
+ ):
250
+ self.tool_id = tool_id
251
+ self.success = success
252
+ self.output = output
253
+ self.error_message = error_message
254
+ self.exit_code = exit_code
255
+ self.tool_name_resolved: str = "Tool"
256
+
257
+ def render(self, log_prefix: str) -> str:
258
+ if self.output:
259
+ truncated = truncate_content(self.output)
260
+ return f"{log_prefix} \033[32m[Tool Result] {truncated}\033[0m"
261
+ if self.error_message:
262
+ return f"{log_prefix} \033[31m[Tool Error] {self.error_message}\033[0m"
263
+ if self.success:
264
+ return f"{log_prefix} \033[32m{self.tool_name_resolved} ran successfully\033[0m"
265
+ return f"{log_prefix} \033[31m[Tool Error] {self.tool_name_resolved} failed\033[0m"
266
+
267
+
268
+ class ErrorEvent(CopilotEvent):
269
+ """Session-level error event."""
270
+
271
+ def __init__(self, message: str, error_type: str | None = None):
272
+ self.message = message
273
+ self.error_type = error_type
274
+
275
+ def render(self, log_prefix: str) -> str:
276
+ label = f"{self.error_type}: " if self.error_type else ""
277
+ return f"{log_prefix} \033[31m[Error] {label}{self.message}\033[0m"
278
+
279
+
280
+ class ResultEvent(CopilotEvent):
281
+ """Top-level non-interactive result summary."""
282
+
283
+ def __init__(self, session_id: str | None, exit_code: int | None, usage: dict[str, Any] | None = None):
284
+ self.session_id = session_id
285
+ self.exit_code = exit_code
286
+ self.usage = usage or {}
287
+
288
+ def render(self, log_prefix: str) -> str | None:
289
+ return None
@@ -0,0 +1,29 @@
1
+ """Compatibility shim for older ``agentshim.copilot_events`` imports."""
2
+
3
+ from .copilot.events import (
4
+ CopilotEvent,
5
+ ErrorEvent,
6
+ IntentEvent,
7
+ MessageDeltaEvent,
8
+ MessageEvent,
9
+ ResultEvent,
10
+ SessionStartEvent,
11
+ ToolResultEvent,
12
+ ToolUseEvent,
13
+ TurnEndEvent,
14
+ UsageEvent,
15
+ )
16
+
17
+ __all__ = [
18
+ "CopilotEvent",
19
+ "ErrorEvent",
20
+ "IntentEvent",
21
+ "MessageDeltaEvent",
22
+ "MessageEvent",
23
+ "ResultEvent",
24
+ "SessionStartEvent",
25
+ "ToolResultEvent",
26
+ "ToolUseEvent",
27
+ "TurnEndEvent",
28
+ "UsageEvent",
29
+ ]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentshim"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "Provider-agnostic coding-agent CLI shims"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -0,0 +1,11 @@
1
+ {"type":"session.mcp_server_status_changed","data":{"serverName":"github-mcp-server","status":"connected"},"id":"ac33a30f-af73-4ed8-80f7-f09b383b247b","timestamp":"2026-04-23T06:53:56.847Z","parentId":"aa1cfd37-9c6a-40ca-ad3c-bc70fdf8bb54","ephemeral":true}
2
+ {"type":"session.mcp_servers_loaded","data":{"servers":[{"name":"github-mcp-server","status":"connected","source":"builtin"}]},"id":"b3d155a6-7724-4590-b5e1-2d186b4798d0","timestamp":"2026-04-23T06:53:57.036Z","parentId":"cb9d73a1-75de-4a6c-af7e-01a3f60cc170","ephemeral":true}
3
+ {"type":"session.mcp_servers_loaded","data":{"servers":[{"name":"github-mcp-server","status":"connected","source":"builtin"}]},"id":"d53123ef-3fe2-453c-ac8a-f2efea54c721","timestamp":"2026-04-23T06:53:57.037Z","parentId":"cb9d73a1-75de-4a6c-af7e-01a3f60cc170","ephemeral":true}
4
+ {"type":"session.skills_loaded","data":{"skills":[{"name":"customize-cloud-agent","description":"Skill for customizing the Copilot cloud agent (formerly known as Copilot coding agent) environment, including copilot-setup-steps.yml configuration, preinstalling tools and dependencies, runners, and settings.","source":"builtin","userInvocable":false,"enabled":true,"path":"/home/shli/.cache/copilot/pkg/linux-x64/1.0.34/builtin-skills/customize-cloud-agent/SKILL.md"}]},"id":"c3189a84-d0b1-4aa3-b906-92cd01f5c6fc","timestamp":"2026-04-23T06:53:57.054Z","parentId":"cb9d73a1-75de-4a6c-af7e-01a3f60cc170","ephemeral":true}
5
+ {"type":"session.mcp_servers_loaded","data":{"servers":[{"name":"github-mcp-server","status":"connected","source":"builtin"}]},"id":"e6e3ec03-ad6a-4e70-8a42-cb3e3bfd82ca","timestamp":"2026-04-23T06:53:58.212Z","parentId":"cb9d73a1-75de-4a6c-af7e-01a3f60cc170","ephemeral":true}
6
+ {"type":"session.tools_updated","data":{"model":"gpt-4.1"},"id":"6b97da6a-cd9b-4bb1-a38c-938f438021ba","timestamp":"2026-04-23T06:53:58.225Z","parentId":"e4fb6ec2-196e-4044-ac50-3f001e682bea","ephemeral":true}
7
+ {"type":"user.message","data":{"content":"Remember the word CEDAR. Reply with exactly STORED and nothing else.","transformedContent":"<current_datetime>2026-04-23T06:53:58.229+00:00</current_datetime>\n\nRemember the word CEDAR. Reply with exactly STORED and nothing else.\n\n<reminder>\n<sql_tables>No tables currently exist. Default tables (todos, todo_deps) will be created automatically when you first use the SQL tool.</sql_tables>\n</reminder>","attachments":[],"supportedNativeDocumentMimeTypes":[],"interactionId":"9606351c-ffc7-4fec-bf69-42ee96ec9659"},"id":"b9ab8585-78d4-4aed-b963-dbd3a0fe5007","timestamp":"2026-04-23T06:53:58.229Z","parentId":"e4fb6ec2-196e-4044-ac50-3f001e682bea"}
8
+ {"type":"assistant.turn_start","data":{"turnId":"0","interactionId":"9606351c-ffc7-4fec-bf69-42ee96ec9659"},"id":"6052f203-493e-4a7b-aa96-94e8a3089c84","timestamp":"2026-04-23T06:53:58.246Z","parentId":"b9ab8585-78d4-4aed-b963-dbd3a0fe5007"}
9
+ {"type":"assistant.message","data":{"messageId":"820e17d9-4774-4177-bc49-151c30d209dc","content":"STORED","toolRequests":[],"interactionId":"9606351c-ffc7-4fec-bf69-42ee96ec9659","outputTokens":5,"requestId":"8AE6:34351:1619720:1992F62:69E9C129"},"id":"a06c95a9-52cc-4f1c-8790-0b7400a80ef2","timestamp":"2026-04-23T06:54:02.234Z","parentId":"6052f203-493e-4a7b-aa96-94e8a3089c84"}
10
+ {"type":"assistant.turn_end","data":{"turnId":"0"},"id":"c6592e58-599c-44e2-9211-7d7541acdd25","timestamp":"2026-04-23T06:54:02.236Z","parentId":"a06c95a9-52cc-4f1c-8790-0b7400a80ef2"}
11
+ {"type":"result","timestamp":"2026-04-23T06:54:02.355Z","sessionId":"33333333-3333-4333-8333-333333333333","exitCode":0,"usage":{"premiumRequests":0,"totalApiDurationMs":2743,"sessionDurationMs":6363,"codeChanges":{"linesAdded":0,"linesRemoved":0,"filesModified":[]}}}