feat: add GitHub Copilot CLI adapter#1
Conversation
Add CopilotCLIAdapter implementing the AgentAdapter protocol, enabling agent-shell to execute Copilot CLI as a supported agent type alongside Claude Code and OpenCode. Key details: - Maps Copilot CLI NDJSON events (reasoning_delta, message_delta, result, etc.) to shared StreamEvent types - Requires --enable-reasoning-summaries flag for thinking/reasoning events - Cost defaults to 0.0 (Copilot doesn't expose pricing) - Session ID extracted from the result event (not turn_start) - Uses --allow-all-tools by default for auto-approve, individual --allow-tool flags when allowed_tools list is specified 148 unit+integration tests + 10 E2E tests all passing (174 total).
ScottRBK
left a comment
There was a problem hiding this comment.
Review
Overall: solid adapter, cleanly mirrors OpenCodeAdapter, good test coverage (unit + integration + E2E). One behavioural item worth addressing before merge, plus minor notes.
assistant.turn_start emits a payload-less system event
copilot_cli_adapter.py:195-196:
if t == "assistant.turn_start":
events.append(StreamEvent(type="system", content=""))Copilot's turn_start payload has no sessionId (only result does — see copilot_fixtures.py:893), so this yields StreamEvent(type="system", content="", session_id=None) — zero information.
There's a direct precedent in claude_code_adapter.py:140-143 which guards emission on session_id:
if t == "system":
if session_id:
events.append(StreamEvent(type="system", content="", session_id=session_id))Across the three adapters, type="system" is implicitly a session-id carrier, not a lifecycle marker. An empty one is invisible to execute()'s next((e.session_id for e in chunks if e.session_id), None) harvest and misleading to consumers filtering by type.
Suggestion: drop the turn_start branch entirely. The result branch already emits session_id correctly.
duration isn't surfaced on AgentResponse
README says duration is populated from usage.totalApiDurationMs, but AgentResponse has no duration field — only the StreamEvent carries it, so execute() callers can't see it. Either add it to AgentResponse (cross-adapter change) or tighten the README wording.
Minor
test_copilot_cli_cancel.pydoesn't assertunregister_process_groupwas called.
…tResponse, assert unregister_process_group
Summary
CopilotCLIAdapterimplementing theAgentAdapterprotocol for GitHub Copilot CLIreasoning_delta,message_delta,result, etc.) to sharedStreamEventtypesKey design decisions
--enable-reasoning-summariesCLI flag; disabled by default wheninclude_thinking=False0.0(Copilot doesn't expose pricing data)resultevent (notturn_start— a behavioral difference from Claude Code/OpenCode)--allow-all-toolsby default; individual--allow-toolflags whenallowed_toolsis specifiedFiles
src/agent_shell/adapters/copilot_cli_adapter.pytests/unit/copilot_fixtures.pytests/unit/test_copilot_cli_*.pytests/integration/test_copilot_cli_integration.pytests/e2e/test_copilot_cli_e2e.pysrc/agent_shell/shell.py_resolve_adapter()README.md