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.
- {agentshim-0.2.0 → agentshim-0.3.0}/PKG-INFO +1 -1
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/__init__.py +2 -0
- agentshim-0.3.0/agentshim/copilot/__init__.py +3 -0
- agentshim-0.3.0/agentshim/copilot/agent.py +290 -0
- agentshim-0.3.0/agentshim/copilot/events.py +289 -0
- agentshim-0.3.0/agentshim/copilot_events.py +29 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/pyproject.toml +1 -1
- agentshim-0.3.0/tests/fixtures/copilot/session_turn_1.jsonl +11 -0
- agentshim-0.3.0/tests/fixtures/copilot/session_turn_2_resumed.jsonl +11 -0
- agentshim-0.3.0/tests/fixtures/copilot/streaming_dedup.jsonl +5 -0
- agentshim-0.3.0/tests/fixtures/copilot/tool_and_usage.jsonl +6 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/cli_agent/test_cli_prompt_passing.py +26 -1
- agentshim-0.3.0/tests/unit/test_agent_cli_copilot.py +168 -0
- agentshim-0.3.0/tests/unit/test_agent_cli_copilot_fixtures.py +99 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_agent_cli_event_parsing.py +65 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_agent_cli_resume.py +59 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_cli_agent_usage.py +37 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/uv.lock +1 -1
- {agentshim-0.2.0 → agentshim-0.3.0}/.github/workflows/ci.yml +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/.github/workflows/publish.yml +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/.gitignore +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/README.md +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/base.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/claude/__init__.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/claude/agent.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/claude/events.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/claude/hooks/__init__.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/claude/hooks/confine_reads.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/claude_events.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/cli_agent.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/codex/__init__.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/codex/agent.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/codex/events.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/codex_events.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/events.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/gemini/__init__.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/gemini/agent.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/gemini/events.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/gemini_events.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/llm_client.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/mcp_config.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/opencode/__init__.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/opencode/agent.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/opencode/events.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/opencode_events.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/py.typed +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/sandbox.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/subagent.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/trajectory.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/usage.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/agentshim/utils.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/cli_agent/test_agent_cli_cleanup.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/cli_agent/test_check_cli.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/llm/conftest.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/llm/test_claude_stream.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/llm/test_gemini_fixture.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/llm/test_gemini_stream.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_agent_cli_claude.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_agent_cli_codex.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_agent_cli_mcp_unsupported.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_agent_cli_sandbox.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_coding_agent_facade.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_coding_agent_recorder_default.py +0 -0
- {agentshim-0.2.0 → agentshim-0.3.0}/tests/unit/test_mcp_config.py +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,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
|
+
]
|
|
@@ -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":[]}}}
|